Configuration
Reference for every config file shipped with oxide-dispatch.
Reference for every config file shipped with oxide-dispatch. All config files live under shared/ and are listed in escrow_ignore so they remain editable.
shared/config.lua
Global runtime settings — broadcast policy, popup behavior, auto-expiry, and the heatmap cache.
| Key | Type | Default | Description |
|---|---|---|---|
Config.AutoExpire.enabled | boolean | true | Toggles the background expiry sweeper |
Config.AutoExpire.defaultMinutes | number | 15 | TTL applied to new alerts (in minutes) |
Config.AutoExpire.sweepIntervalSec | number | 30 | How often the sweeper runs (in seconds) |
Config.Broadcast.onDutyOnly | boolean | true | Only deliver alerts to on-duty responders. Defers to oxide-police for LEO duty when installed |
Config.Broadcast.maxJobsPerAlert | number | 12 | Hard cap on the number of resolved jobs per alert (extras are trimmed) |
Config.Broadcast.rateLimitMs | number | 5000 | Minimum gap between client-originated alerts per player |
Config.DispatchHub.RadialEnabled | boolean | true | Master switch for the Dispatch Hub entry inside the Dispatch radial menu. Disable on servers where another resource (e.g. oxide-police MDT) already provides a dispatch view |
Config.Popup.durationMs | number | 20000 | How long each popup stays on screen (ms) |
Config.Popup.maxStacked | number | 3 | Maximum number of stacked popups visible at once |
Config.Popup.mapZoom | number | 3 | Zoom level used by the popup mini-map |
Config.Popup.soundEffect | string | 'Lose_1st' | Default sound effect played on alert receive |
Config.Popup.soundSet | string | 'GTAO_FM_Events_Soundset' | Sound set the effect is loaded from |
Config.Heatmap.enabled | boolean | true | Enables the heatmap data feed in the Dispatch Hub |
Config.Heatmap.defaultWindow | string | '30d' | Default time window. One of '24h', '7d', '30d', 'all' |
Config.Heatmap.cacheSeconds | number | 60 | TTL of the in-memory heatmap cache (per window) |
Config.Heatmap.maxPoints | number | 5000 | Max points returned per query (downsampled if exceeded) |
Config.AutoExpire = {
enabled = true,
defaultMinutes = 15,
sweepIntervalSec = 30,
}
Config.Broadcast = {
onDutyOnly = true,
maxJobsPerAlert = 12,
rateLimitMs = 5000,
}
Config.DispatchHub = {
RadialEnabled = true,
}
Config.Popup = {
durationMs = 20000,
maxStacked = 3,
mapZoom = 3,
soundEffect = 'Lose_1st',
soundSet = 'GTAO_FM_Events_Soundset',
}
Config.Heatmap = {
enabled = true,
defaultWindow = '30d',
cacheSeconds = 60,
maxPoints = 5000,
}shared/config/alerts.lua
The dispatch code catalog. Producers supply a code field on every alert; this catalog supplies the defaults (title, priority, icon, blip) for any field the producer does not explicitly populate. Per-alert sound overrides are also supported via data.soundEffect / data.soundSet.
Config.Codes
A mapping of code → { title, priority, icon, blip }. Each entry's blip is { sprite, color, scale }. Priority 1 is highest.
| Code | Title | Priority | Icon | Category |
|---|---|---|---|---|
10-11 | Traffic Stop | 3 | fas fa-car | Patrol / routine |
10-14 | Prowler | 3 | fas fa-user-secret | Patrol / routine |
10-66 | Suspicious Activity | 3 | fas fa-magnifying-glass | Patrol / routine |
10-30 | Grand Theft Auto | 2 | fas fa-key | Crime in progress |
10-31 | Crime in Progress | 2 | fas fa-handcuffs | Crime in progress |
10-34 | Assault in Progress | 2 | fas fa-hand-fist | Crime in progress |
10-91 | Store Robbery | 2 | fas fa-sack-dollar | Crime in progress |
10-99 | Narcotics Activity | 2 | fas fa-cannabis | Crime in progress |
10-50 | Vehicle Accident | 2 | fas fa-car-burst | Vehicle |
10-55 | Intoxicated Driver | 2 | fas fa-wine-bottle | Vehicle |
10-57 | Hit and Run | 2 | fas fa-car-side | Vehicle |
10-32 | Person With a Gun | 1 | fas fa-gun | High priority |
10-71 | Shots Fired | 1 | fas fa-gun | High priority |
10-72 | Drive-By Shooting | 1 | fas fa-gun | High priority |
10-77 | Explosion | 1 | fas fa-bomb | High priority |
10-80 | Pursuit | 1 | fas fa-car-side | High priority |
10-90 | Silent Alarm | 1 | fas fa-building-columns | High priority |
10-52 | Medical Emergency | 2 | fas fa-truck-medical | EMS |
10-53 | Person Down | 1 | fas fa-person-falling | EMS |
10-54 | Cardiac Arrest | 1 | fas fa-heart-pulse | EMS |
10-56 | Trauma / Injury | 2 | fas fa-user-injured | EMS |
10-58 | Overdose | 1 | fas fa-syringe | EMS |
10-70 | Structure Fire | 1 | fas fa-fire | Fire / hazmat |
10-73 | Vehicle Fire | 1 | fas fa-car-burst | Fire / hazmat |
10-74 | Brush / Wildfire | 1 | fas fa-fire-flame-curved | Fire / hazmat |
10-75 | Hazmat Incident | 1 | fas fa-radiation | Fire / hazmat |
10-76 | Rescue / Extrication | 1 | fas fa-hands-holding | Fire / hazmat |
10-13 | Officer Needs Backup | 1 | fas fa-shield-halved | Officer safety |
panic | Panic Button | 1 | fas fa-bell | Officer safety |
911 | 911 Call | 2 | fas fa-phone-volume | Citizen-initiated |
311 | 311 Tip | 4 | fas fa-comment-dots | Citizen-initiated |
Config.GetCodeDefaults(code)
Helper used by the alert pipeline. Returns the catalog entry for the code, or a generic fallback (title = code, priority = 3, icon = 'fas fa-triangle-exclamation', generic blip) when the code is not in the catalog.
local defaults = Config.GetCodeDefaults('10-71')
-- { title = 'Shots Fired', priority = 1, icon = 'fas fa-gun', blip = {...} }To add a new code, drop a new entry into Config.Codes:
Config.Codes['10-100'] = {
title = 'Burglary in Progress',
priority = 2,
icon = 'fas fa-house-lock',
blip = { sprite = 161, color = 1, scale = 1.1 },
-- optional per-code sound override:
-- soundEffect = '...', soundSet = '...',
}shared/config/jobs.lua
Maps responder group keys (police, ems, fire) to framework job names plus the UI metadata used by the Dispatch radial menu and panic title resolver.
Config.ResponderGroups
Each entry defines:
| Field | Type | Description |
|---|---|---|
jobs | string[] | Framework job names that count as members of this group |
label | string | Human-readable label used in dispatch UI and panic titles |
icon | string | Font Awesome icon name (no fas fa- prefix) |
Config.ResponderGroups = {
police = {
jobs = { 'police', 'lspd', 'bcso', 'sasp', 'sheriff', 'statetrooper',
'LSPD', 'BCSO', 'SASP' },
label = 'Officer',
icon = 'shield-halved',
},
ems = {
jobs = { 'ambulance', 'ems', 'EMS' },
label = 'Paramedic',
icon = 'truck-medical',
},
fire = {
jobs = { 'fire', 'FIRE' },
label = 'Firefighter',
icon = 'fire',
},
}A
radialIdfield from older configs is ignored — all groups now share the single Dispatch radial menu. You can safely delete the field.
Job-name matching is case-sensitive. Include every casing your framework actually uses.
Config.ResolveJobsDefault
Fallback used when a producer omits jobs. nil (the default) means an alert with no jobs reaches no recipients and a one-time warning is logged. Set to a group key (e.g. 'police') to opt back into the legacy silent-fallback behavior.
Config.ResolveJobsDefault = nil -- or 'police' to silently defaultConfig.ResolveJobs(input)
Normalizes producer inputs into a flat job-name array.
| Input | Returns |
|---|---|
nil | Config.ResolveJobsDefault (or empty + warning) |
'police' (group key) | The group's jobs array |
'sheriff' (raw job) | { 'sheriff' } |
{ 'police', 'sasp' } | Deduped expansion of all values |
Config.GetGroupForJob(jobName)
Reverse lookup. Returns the group key ('police', 'ems', 'fire') that owns a given raw job name, or nil if it isn't grouped. Used by the broadcast layer, panic subsystem, and per-group status broadcasts.
Adding a new responder group
To add a new responder group (for example coastguard):
- Add the group key + jobs to
Config.ResponderGroupshere. - Add a status list under the same key in
Config.ResponderStatuses(or rely on_default). - (Optional) Add a
titles/messagesentry for the group inConfig.Panicso panic alerts read naturally.
Full recipes (add a job, rename a job, build a new group) live in the Jobs & Responder Groups guide.
shared/config/statuses.lua
Per-session responder status states. Each responder group has its own status list so police-coded labels (Code-4, 10-7) don't leak into EMS/Fire UIs. Status changes broadcast to colleagues in the SAME group.
Config.ResponderStatuses
Each entry is a list of { id, label, color, icon } rows.
| Group | Statuses (id → label) |
|---|---|
police | available → Available, busy → Busy / On Call, code4 → Code-4, 10-7 → 10-7 (Out of Service) |
ems | available, oncall, enroute, onscene, transport, 10-7 |
fire | available, enroute, onscene, returning, 10-7 |
_default | available, busy, 10-7 (used when a custom group has no specific list) |
Config.ResponderStatusDefault
| Key | Type | Default | Description |
|---|---|---|---|
Config.ResponderStatusDefault | string | 'available' | Status id new responders are initialized with. Must exist in every group's status list (and in _default) |
Helpers
| Function | Returns | Description |
|---|---|---|
Config.GetResponderStatusList(group) | table[] | The status list for a group (falls back to _default) |
Config.GetResponderStatus(group, id) | table|nil | Resolves a status entry by group + id, with fallback to _default |
Config.ResponderCallsign
| Key | Type | Default | Description |
|---|---|---|---|
Config.ResponderCallsign.maxLength | number | 16 | Maximum callsign length (chars). Enforced server-side and on the input dialog |
Config.ResponderCallsign.pattern | string | '^[%w%-]+$' | Lua string pattern (anchored) callsigns must match. Extend the character class to allow dots, slashes, etc. |
Config.ResponderCallsign = {
maxLength = 16,
pattern = '^[%w%-]+$', -- alphanumerics + hyphen
-- pattern = '^[%w%-%.]+$', -- alphanumerics + hyphen + dot ('A-12.5')
-- pattern = '^[%w%-/%.]+$', -- alphanumerics + hyphen + slash + dot ('EMS/01')
}shared/config/detection.lua
Toggles the automatic crime detection module. Each detector funnels into the standard alert pipeline (rate limiting + duty filter + persistence apply normally).
Config.Detection
Every detector accepts a jobs field for recipient routing. It takes the same input as Config.ResolveJobs: a responder-group key ('police'), a raw job name ('lspd'), or an array of either.
| Key | Type | Default | Description |
|---|---|---|---|
Config.Detection.rateLimitPerPlayerMs | number | 8000 | Fallback per-player cooldown (ms), used only for detectors that have no entry in cooldownsMs |
Config.Detection.cooldownsMs.shooting | number | 12000 | Per-player cooldown (ms) between shots-fired alerts |
Config.Detection.cooldownsMs.driveby | number | 15000 | Per-player cooldown (ms) between drive-by alerts |
Config.Detection.cooldownsMs.melee | number | 60000 | Per-player cooldown (ms) between melee alerts |
Config.Detection.cooldownsMs.explosion | number | 15000 | Per-player cooldown (ms) between explosion alerts |
Config.Detection.cooldownsMs.vehicletheft | number | 30000 | Per-player cooldown (ms) between vehicle-theft alerts |
Config.Detection.ServerDedup.enabled | boolean | true | Server-side duplicate suppression — drops repeated auto-detect alerts of the same code from the same offender, even if a modified client skips its own cooldown |
Config.Detection.ServerDedup.windowMs | number | 60000 | How long (ms) repeats of the same alert from the same offender are suppressed |
Config.Detection.Shooting.enabled | boolean | true | Detect shots fired on foot |
Config.Detection.Shooting.code | string | '10-71' | Code used for the alert |
Config.Detection.Shooting.jobs | string|table | 'police' | Recipient group(s) |
Config.Detection.DriveBy.enabled | boolean | true | Detect shots fired from a vehicle |
Config.Detection.DriveBy.code | string | '10-72' | Code used for the alert |
Config.Detection.DriveBy.jobs | string|table | 'police' | Recipient group(s) |
Config.Detection.Melee.enabled | boolean | true | Detect melee combat (witness-based) |
Config.Detection.Melee.code | string | '10-34' | Code used for the alert |
Config.Detection.Melee.jobs | string|table | 'police' | Recipient group(s) |
Config.Detection.Explosion.enabled | boolean | true | Detect explosions |
Config.Detection.Explosion.code | string | '10-77' | Code used for the alert |
Config.Detection.Explosion.jobs | string|table | {'police','ems','fire'} | Recipient group(s) |
Config.Detection.Death.enabled | boolean | true | Auto-dispatch on player death |
Config.Detection.Death.code | string | '10-52' | Code used for the alert |
Config.Detection.Death.jobs | string|table | 'ems' | Recipient group(s) |
Config.Detection.VehicleTheft.enabled | boolean | true | Detect car-alarm trip + carjacking + oxide-vehicles lockpick/hotwire |
Config.Detection.VehicleTheft.code | string | '10-30' | Code used for the alert |
Config.Detection.VehicleTheft.jobs | string|table | 'police' | Recipient group(s) |
Every detector also accepts chance (number, default 1.0) — the 0.0–1.0 probability that a qualifying event is reported (0.0 disables it, like enabled = false). For the cooldown-gated detectors the roll happens once per cooldown window; Death rolls once per death.
Config.Detection.WeaponBlacklist
Weapons that should never trigger an automatic alert (less-lethal, training weapons, fists). Each key is a weapon, each value is true. Write the weapon name inside backticks (e.g. `WEAPON_PISTOL`) — the game converts the backticked name into the weapon hash it uses internally.
Config.Detection.WeaponBlacklist = {
[`WEAPON_UNARMED`] = true,
[`WEAPON_STUNGUN`] = true,
[`WEAPON_FLAREGUN`] = true,
}Config.Detection.ZoneBlacklist
Geographic exclusion zones — shooting/explosion in these areas won't alert. Each entry: { coords = vector3, radius = number, label = string }.
Config.Detection.ZoneBlacklist = {
-- { coords = vector3(450.0, -980.0, 30.0), radius = 60.0, label = 'Mission Row PD' },
-- { coords = vector3(842.0, -1290.0, 28.0), radius = 80.0, label = 'PD Range' },
}Config.Detection.JobBlacklist
Jobs whose holders should never trip an auto-detect alert. Keys are framework job names and must exactly match the job name as your framework spells it (case-sensitive); values true. Use this to stop on-duty responders from dispatching themselves whenever they fire their service weapon, commandeer a vehicle in pursuit, or take a melee hit on the job. Manual paths (911, panic, MDT-issued alerts) ignore this list.
Config.Detection.JobBlacklist = {
-- ['police'] = true,
-- ['lspd'] = true,
-- ['bcso'] = true,
-- ['sasp'] = true,
-- ['sheriff'] = true,
-- ['statetrooper'] = true,
-- ['ambulance'] = true,
-- ['ems'] = true,
-- ['fire'] = true,
}shared/config/panic.lua
Two distinct subsystems live in this file: the panic / responder-needs-backup alert (separate from the panic dispatch code) and the civilian 911/311 category catalog.
Config.Panic
| Key | Type | Default | Description |
|---|---|---|---|
Config.Panic.cooldownSec | number | 60 | Per-responder cooldown (seconds) on the panic keybind / /panic |
Config.Panic.broadcastOffDuty | boolean | true | When true, panic alerts ignore Config.Broadcast.onDutyOnly |
Config.Panic.autoWaypoint | boolean | true | Auto-set the recipient's GPS waypoint to the panicker |
Config.Panic.soundEffect | string|nil | nil | Distinct alert sound for panic. Falls back to Config.Popup.soundEffect when nil |
Config.Panic.soundSet | string|nil | nil | Sound set for the panic alert. Falls back to Config.Popup.soundSet when nil |
Config.Panic.titles[group] | string | per group | Panic alert title per responder group. _default is the fallback |
Config.Panic.messages[group] | string | per group | Panic alert message per responder group. _default is the fallback |
Config.Panic = {
cooldownSec = 60,
broadcastOffDuty = true,
autoWaypoint = true,
soundEffect = nil,
soundSet = nil,
titles = {
police = 'OFFICER NEEDS BACKUP',
ems = 'MEDIC NEEDS BACKUP',
fire = 'FIREFIGHTER NEEDS BACKUP',
_default = 'RESPONDER NEEDS BACKUP',
},
messages = {
police = 'Officer panic alert',
ems = 'Medic panic alert',
fire = 'Firefighter panic alert',
_default = 'Responder panic alert',
},
}Config.Civilian
Configures the citizen 911 / 311 menu (radial + chat commands).
| Key | Type | Default | Description |
|---|---|---|---|
Config.Civilian.anonymousDefault | boolean | true | Whether 911/311 calls strip caller identity from the broadcast by default |
Config.Civilian.maxMessageLength | number | 240 | Hard cap on civilian call message + responder reply length (chars). Truncated server-side |
Config.Civilian.Categories911 | table[] | see file | Category list for /911 and the radial Call 911 submenu |
Config.Civilian.Categories311 | table[] | see file | Category list for /311 and the radial Call 311 submenu |
Each category entry is { id, label, code, jobs, icon? }. jobs accepts the same input Config.ResolveJobs does (group key, raw job, or array).
Config.Civilian = {
anonymousDefault = true,
maxMessageLength = 240,
Categories911 = {
{ id = 'crime', label = 'Crime in Progress', code = '10-31', jobs = 'police' },
{ id = 'medical', label = 'Medical Emergency', code = '10-52', jobs = 'ems' },
{ id = 'injury', label = 'Person Injured', code = '10-56', jobs = 'ems' },
{ id = 'overdose', label = 'Overdose', code = '10-58', jobs = 'ems' },
{ id = 'fire', label = 'Structure Fire', code = '10-70', jobs = 'fire' },
{ id = 'vfire', label = 'Vehicle Fire', code = '10-73', jobs = 'fire' },
{ id = 'hazmat', label = 'Hazmat / Spill', code = '10-75', jobs = 'fire' },
{ id = 'shooting', label = 'Shots Fired', code = '10-71', jobs = 'police' },
{ id = 'accident', label = 'Vehicle Accident', code = '10-50', jobs = { 'police', 'ems' } },
{ id = 'other', label = 'Other Emergency', code = '911', jobs = { 'police', 'ems', 'fire' } },
},
Categories311 = {
{ id = 'tip', label = 'Anonymous Tip', code = '311', jobs = 'police' },
{ id = 'noise', label = 'Noise Complaint', code = '311', jobs = 'police' },
{ id = 'suspicious', label = 'Suspicious Person', code = '10-66', jobs = 'police' },
{ id = 'medical_q', label = 'Medical Question', code = '311', jobs = 'ems' },
{ id = 'fire_safety', label = 'Fire / Safety Issue', code = '311', jobs = 'fire' },
{ id = 'other', label = 'Other Non-Emergency', code = '311', jobs = 'police' },
},
}shared/config/compat.lua
Per-framework toggles for the legacy police-alert shims. The shims forward old QB/ESX events into the modern Alerts.Create pipeline so existing scripts keep working without edits.
Top-level toggles
| Key | Type | Default | Description |
|---|---|---|---|
Config.Compatibility.enabled | boolean | true | Master toggle for all shims |
Config.Compatibility['qb-core']
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Enable the QBCore shim |
serverPoliceAlert | boolean | true | Listen for police:server:policeAlert from server scripts |
clientPoliceAlert | boolean | false | Listen for the QB client-side police-alert relay |
ambulanceAlert | boolean | true | Listen for hospital:server:ambulanceAlert (qb-ambulancejob / qbx_ambulancejob downed alerts, /911e, third-party) |
emergencyAlert | boolean | true | Listen for hospital:server:emergencyAlert (QBX radial menu fast path) |
clientAmbulanceAlert | boolean | false | Listen for the QB client-side ambulance-alert relay |
emsJobs | string|table | 'ems' | Group / job(s) the converted ambulance alert is addressed to |
emsCode | string | '10-52' | Code used for converted ambulance alerts |
jobs | string|table | 'police' | Group / job(s) the converted police alert is addressed to |
defaultCode | string | '10-31' | Code used when no keyword matches the police alert message |
rateLimitMs | number | 5000 | Per-source rate limit for the shim (shared across police + EMS paths) |
mirrorQbPhone | boolean | false | When true and qb-phone is started, mirror the police alert into qb-phone:client:addPoliceAlert |
keywordCodes | table | see file | Ordered keyword → code rules for the police shim (case-insensitive substring match) |
When
Config.Detection.Death.enabledANDambulanceAlertare both on, the sharedrateLimitMsabsorbs the duplicate alert that fires when a player dies. If you setrateLimitMs = 0, disable one of the two to avoid duplicates.
Config.Compatibility['qb-core'] = {
enabled = true,
serverPoliceAlert = true,
clientPoliceAlert = false,
ambulanceAlert = true,
emergencyAlert = true,
clientAmbulanceAlert = false,
emsJobs = 'ems',
emsCode = '10-52',
jobs = 'police',
defaultCode = '10-31',
rateLimitMs = 5000,
mirrorQbPhone = false,
keywordCodes = {
{ pattern = 'drug', code = '10-99' },
{ pattern = 'narcotic', code = '10-99' },
{ pattern = 'store', code = '10-91' },
{ pattern = 'bank', code = '10-90' },
{ pattern = 'jewel', code = '10-90' },
{ pattern = 'suspicious', code = '10-66' },
{ pattern = 'suspicous', code = '10-66' }, -- common misspelling
{ pattern = 'vehicle', code = '10-80' },
{ pattern = 'robbery', code = '10-31' },
},
}Config.Compatibility['es_extended']
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Enable the ESX shim |
serverPoliceAlert | boolean | true | Listen for esx_policejob:alert / esx_policejob:policeAlert |
clientPoliceAlert | boolean | false | Listen for the ESX client-side police-alert relay |
esxPhoneSend | boolean | true | Convert police-contact esx_phone:send calls to alerts |
outlawAlert | boolean | true | Listen for esx_outlawalert:policeNotify / outlawNotify |
distressAlert | boolean | true | Listen for esx_ambulancejob:onPlayerDistress (downed-player keybind) |
esxPhoneEms | boolean | true | Convert ambulance-contact esx_phone:send calls to alerts |
emsJobs | string|table | 'ems' | Group / job(s) the converted ambulance alert is addressed to |
emsCode | string | '10-52' | Code used for converted ambulance alerts |
jobs | string|table | 'police' | Group / job(s) the converted police alert is addressed to |
defaultCode | string | '10-31' | Code used when no keyword matches the police alert message |
rateLimitMs | number | 5000 | Per-source rate limit for the shim (shared across police + EMS paths) |
keywordCodes | table | see file | Ordered keyword → code rules for the police shim |
Config.Compatibility['es_extended'] = {
enabled = true,
serverPoliceAlert = true,
clientPoliceAlert = false,
esxPhoneSend = true,
outlawAlert = true,
distressAlert = true,
esxPhoneEms = true,
emsJobs = 'ems',
emsCode = '10-52',
jobs = 'police',
defaultCode = '10-31',
rateLimitMs = 5000,
keywordCodes = {
{ pattern = 'drug', code = '10-99' },
{ pattern = 'narcotic', code = '10-99' },
{ pattern = 'store', code = '10-91' },
{ pattern = 'bank', code = '10-90' },
{ pattern = 'jewel', code = '10-90' },
{ pattern = 'suspicious', code = '10-66' },
{ pattern = 'suspicous', code = '10-66' },
{ pattern = 'vehicle', code = '10-80' },
{ pattern = 'robbery', code = '10-31' },
{ pattern = 'shot', code = '10-71' },
{ pattern = 'gun', code = '10-32' },
},
}QBX
QBX uses the same police:server:policeAlert event names QBCore does (verified across qbx_bankrobbery, qbx_truckrobbery, qbx_houserobbery, qbx_jewelery, qbx_storerobbery, qbx_vehiclekeys, and the legacy qbx_police). The qb-core shim above handles QBX traffic — there is no separate qbx_core block. See the QBX Setup page for QBX-specific notes.
shared/config/ai_dispatch.lua
Configuration for the optional AI Radio Dispatcher feature — provider selection, radio channels (one per department job), idle timeout, identifier whitelist, integration toggles, model and voice choices.
| Key | Type | Default | Description |
|---|---|---|---|
Config.AIDispatch.Enabled | boolean | true | Master switch for the AI feature |
Config.AIDispatch.Provider | string | 'chained' | Global default provider ('realtime' or 'chained'); each channel can override |
Config.AIDispatch.Channels | table | see below | Map of radio channel number → channel entry. If left empty, a warning is logged at startup and the AI dispatch feature turns itself off — the server still starts normally |
Config.AIDispatch.IdleTimeoutMin | number | 10 | Auto-close idle realtime sessions after N minutes (chained mode skips) |
Config.AIDispatch.RateLimit.alerts | number | 3 | Per-officer alert cap per window |
Config.AIDispatch.RateLimit.windowSec | number | 60 | Rate-limit window in seconds |
Config.AIDispatch.Access.IdentifierWhitelist | string[] | {} | Global FiveM identifier allowlist fallback. Per-channel whitelist takes precedence |
Config.AIDispatch.Integrations.Mdt | table[] | { {name='oxide-police',...}, {name='ps-mdt',...}, {name='wasabi_mdt',...}, {name='lb-tablet',...} } | Priority list of MDT backends powering the 8 MDT-flavored AI tools (plate/person/profile/BOLO/custody/on-duty). First entry with enabled ~= false whose named resource is started wins. Reorder to change priority. Write your own bridge per the MDT Bridge Development guide. |
Config.AIDispatch.Realtime.Model | string | 'gpt-realtime' | OpenAI Realtime model id |
Config.AIDispatch.Realtime.Voice | string | 'alloy' | Default Realtime voice if a channel doesn't set its own |
Config.AIDispatch.Realtime.Instructions | string | '' | Populated automatically from ai_dispatch_prompt.lua — leave empty |
Config.AIDispatch.Chained.STT.Provider / .Model | string | 'openai' / 'whisper-1' | Speech-to-text adapter and model |
Config.AIDispatch.Chained.LLM.Provider / .Model | string | 'openai' / 'gpt-4o-mini' | Language-model adapter and model |
Config.AIDispatch.Chained.LLM.MaxHistoryTurns | number | 20 | History window per session |
Config.AIDispatch.Chained.LLM.MaxToolCascade | number | 4 | Cap on chained tool-call loops per transmission |
Config.AIDispatch.Chained.TTS.Provider / .Model | string | 'openai' / 'tts-1' | Text-to-speech adapter and model |
Config.AIDispatch.Chained.TTS.Voice | string | 'onyx' | Default TTS voice if a channel doesn't set its own |
Channel entry fields (Config.AIDispatch.Channels[N])
| Field | Type | Required | Description |
|---|---|---|---|
job | string | yes | Framework job name bound to this channel (e.g. 'lspd', 'bcso', 'sasp', 'ambulance', 'fire'). Police-MDT tools appear on this channel when the job is listed in Config.ResponderGroups.police.jobs |
label | string | no | UI label for the channel |
voice | string | no | Override the default voice for this channel |
provider | string | no | Override the global Provider for this channel ('realtime' or 'chained') |
whitelist | string[] | no | Per-channel identifier allowlist; takes precedence over Access.IdentifierWhitelist when non-empty |
Three channels ship enabled by default: 911 → job 'police' (voice ash), 921 → 'ambulance' (voice shimmer), and 931 → 'fire' (voice sage). Channels 912 (BCSO) and 913 (SASP) are included as commented-out examples.
Important: each channel's
jobmust match a job name that actually exists on your server, spelled exactly as your framework has it (usually lowercase). If your police job isn't calledpolice(e.g.lspd), change channel911'sjobvalue — otherwise no player will pass the channel's job check.
Config.AIDispatch.RadioFX
The radio-static filter applied to the AI dispatcher's voice — band-limiting, light distortion, and a faint background hiss make it sound like a real comms radio instead of a clean synthesized stream.
| Key | Type | Default | Description |
|---|---|---|---|
Enabled | boolean | true | Master toggle for the radio effect |
BandpassLow | number | 400 | Lowest voice frequency let through (Hz). Higher = thinner sound |
BandpassHigh | number | 3400 | Highest voice frequency let through (Hz) |
Distortion | number | 0.04 | Distortion amount, 0–1 (0 = clean, 0.3 = crunchy) |
NoiseFloor | number | 0.008 | Volume of the constant background hiss, 0–1 (0 = off, 0.04 = very loud) |
NoiseLow | number | 1000 | Lowest frequency of the hiss (Hz) |
NoiseHigh | number | 4500 | Highest frequency of the hiss (Hz) |
ChainedGain | number | 1.85 | Output volume of the voice in chained mode (1.0 = unchanged) |
RealtimeGain | number | 1.85 | Output volume of the voice in realtime mode (1.0 = unchanged) |
API key
The OpenAI API key is not stored in this file. Set it as a server convar (a server-side variable, set in server.cfg or the live console):
set oxide:ai-dispatch:openai_key sk-...your-key-here...Use
set, notsetr—setrreplicates the value to every connected player and would leak your key. Other providers read their key fromoxide:ai-dispatch:<provider>_key(e.g.set oxide:ai-dispatch:groq_key ...).
Full feature documentation, cost notes, voice options, identifier-whitelist usage, and privacy disclosure templates live in the AI Radio Dispatcher guide.
shared/config/ai_dispatch_acks.lua
Spoken acknowledgements for the AI dispatcher. When the dispatcher decides to look something up (a plate, a person's records, a BOLO), the chained pipeline can take a few seconds to answer. To avoid dead silence on the radio while that happens, a short pre-rendered acknowledgement plays the moment the lookup starts — "Running that plate.", "Pulling those records.", and so on.
Config.AIDispatch.Acks
| Key | Type | Default | Description |
|---|---|---|---|
Enabled | boolean | true | Master toggle. When false, no clips are rendered at startup and the pipeline behaves as before (silence during lookups) |
Phrases | table | see file | One list of phrases per tool name. A phrase from the matching list is picked at random each time so it doesn't feel scripted — add as many variants as you like |
Default | string[] | { "Copy, stand by.", "Stand by." } | Fallback phrases used when a tool has no Phrases entry (e.g. a new tool added later, or a custom one) |
The audio clips are generated automatically on first server startup using each channel's text-to-speech voice and cached on disk under <resource>/cache/acks/<voice>/. You can add or edit phrases freely — changed phrases are re-rendered on the next boot while unchanged ones are reused from the cache.
Shipped phrase lists cover the core dispatch tools (create_dispatch_alert, cancel_dispatch_alert, attach_responder, detach_responder, set_responder_status, set_officer_status, trigger_panic, list_active_alerts, update_dispatch_alert) and the police MDT tools (lookup_plate, lookup_person, get_person_profile, create_bolo, list_active_bolos, clear_bolo, list_on_duty_officers, check_custody).
shared/config/ai_dispatch_prompt.lua
The dispatcher's system prompt — the behavior contract the AI follows. Built per session by Config.BuildAIDispatchPrompt(job, hasPoliceTools) and used by both the realtime and chained providers.
Structure:
JOB_BLOCKS— per-job lookup of{ name, flavor }.nameis how the dispatcher introduces itself ("LSPD", "BCSO", "EMS", etc.);flavorselects which routing-rules block applies ('police','ems','fire', ornilfor generic). Add a key for any custom job you want to tune.FLAVOR_ROUTING— three blocks of natural-language routing guidance keyed by flavor.BASE_PROMPT,CORE_TOOL_RULES,POLICE_TOOL_RULES,RADIO_AWARENESS— structural sections spliced together byBuildAIDispatchPrompt. Police MDT guidance is only included when the channel's job qualifies for those tools.
Jobs that aren't in JOB_BLOCKS get a generic uppercase fallback (e.g. 'amr_paramedic' → "AMR_PARAMEDIC dispatch"). If you customize the prompt, keep the structural sections intact — the model relies on them to use the tools correctly.