# API examples

Complete recipes you can drop into your own resource. Each one assumes `koja-dispatcher` is started before your resource (see the [overview](/new/dispatcher/api/overview)).

| Recipe | Uses |
|---|---|
| [Phone app 911](#phone-app-911) | `PhoneCall`, `CreateCall`, `Call911` |
| [Store robbery alert with auto-assign](#store-robbery-alert-with-auto-assign) | `CreateIncident`, `CreateCall`, `GetDispatcherCount` |
| [ALPR camera script](#alpr-camera-script) | `ReportPlate` |
| [Priority 1 for Fleeca robberies](#priority-1-for-fleeca-robberies) | `incidentCreate` hook |
| [Block shifts for suspended players](#block-shifts-for-suspended-players) | `shiftStart` hook |
| [Discord or log on incidentEnded](#discord-or-log-on-incidentended) | `koja-dispatcher:incidentEnded`, `DiscordLog` |
| [Field unit status from a job script](#field-unit-status-from-a-job-script) | `RunFieldCode`, `SetUnitStatus`, `GetPlayerUnit` |
| [Units on a custom map](#units-on-a-custom-map) | `GetUnits`, `unitAdded` / `unitRemoved` |
| [Create a BOLO from another script](#create-a-bolo-from-another-script) | `CreateBolo`, `CloseBolo`, `GetBolos` |
| [Escalation in a custom scenario](#escalation-in-a-custom-scenario) | `shared/scenarios/*.lua`, `events`, `stages` |
| [Register your own admin setting](#register-your-own-admin-setting) | `RegisterSetting`, `GetSetting`, `settingChanged` |

---

## Phone app 911

Your phone's 911 app sends the text to its own server event, which hands it to the dispatcher with the player as the caller.

::::codegroup
```lua title="server.lua (PhoneCall)"
-- PhoneCall works with any phone: it uses the /911 logic, so follow-up texts
-- within calls.playerReplyWindow join the open call. Needs phones.enabled.
RegisterNetEvent('myphone:server:call911', function(text)
    local src = source
    if type(text) ~= 'string' then return end

    local id, route = exports['koja-dispatcher']:PhoneCall(src, text)
    if not id then
        if route == 'rate' then
            TriggerClientEvent('myphone:client:notify', src, 'Please wait before calling again.')
        end
        -- 'no_dispatchers': the player already got a "nobody is available" notification
        return
    end
    if route == 'incident' then
        TriggerClientEvent('myphone:client:notify', src, 'Units have been sent to your location.')
    elseif route == 'call' then
        TriggerClientEvent('myphone:client:notify', src, 'Connecting you to the dispatcher...')
    end
end)
```

```lua title="server.lua (CreateCall)"
-- CreateCall gives you full control (caller name, category) but every message
-- is a new call. Works even with phones.enabled off.
local lastCall = {}

RegisterNetEvent('myphone:server:call911', function(text)
    local src = source
    if type(text) ~= 'string' then return end
    text = text:gsub('%c', ' '):sub(1, 250)
    if #text < 3 then return end

    local now = os.time()
    if lastCall[src] and now - lastCall[src] < 30 then return end
    lastCall[src] = now

    local id, route = exports['koja-dispatcher']:CreateCall({
        source = src,                -- position, notifications and replies go to this player
        caller = GetPlayerName(src),
        message = text,
    })
    if not id then
        TriggerClientEvent('myphone:client:notify', src, route == 'no_dispatchers'
            and 'Nobody is answering 911 right now.' or 'The call could not be connected.')
    end
end)

AddEventHandler('playerDropped', function() lastCall[source] = nil end)
```

```lua title="client.lua (Call911)"
-- Call911 is the /911 command, called from your phone UI
RegisterNUICallback('call911', function(data, cb)
    local ok, route = exports['koja-dispatcher']:Call911(tostring(data.text or ''))
    cb({ ok = ok, route = route })
end)
```
::::

:::hint{type="info" title="Supported phones"}
lb-phone, npwd and qs-smartphone-pro are already integrated (`phones.enabled`, `phones.number`): calls and texts to 911 reach the dispatcher without any code, and replies arrive as phone messages. With other phones, replies arrive as normal notifications.
:::

---

## Store robbery alert with auto-assign

The robbery script tells its server side that the alarm went off. The server checks the player is really at the store, then creates the incident and sends the nearest free units.

::::codegroup
```lua title="server.lua"
local stores = {
    ltd_grove = { label = 'LTD Gasoline, Grove Street', coords = vector3(-47.5, -1757.6, 29.4) },
    twentyfour_innocence = { label = '24/7, Innocence Blvd', coords = vector3(25.7, -1347.3, 29.5) },
}
local lastAlarm = {}

RegisterNetEvent('myrobbery:server:alarm', function(storeId)
    local src = source
    local store = stores[storeId]
    if not store then return end

    -- never trust the client: the robber must stand in the store
    if #(GetEntityCoords(GetPlayerPed(src)) - store.coords) > 30.0 then return end

    -- one alarm per store every 10 minutes
    local now = os.time()
    if lastAlarm[storeId] and now - lastAlarm[storeId] < 600 then return end
    lastAlarm[storeId] = now

    local incidentId, err = exports['koja-dispatcher']:CreateIncident({
        category = 'robbery',
        priority = 1,
        coords = store.coords,
        label = store.label,
        need = { police = 2 },
        note = 'Silent alarm, armed robbery in progress',
        autoAssign = true,           -- nearest free units for { police = 2 }
    })
    if not incidentId then
        print(('[myrobbery] alarm failed: %s'):format(err))
    end
end)
```

```lua title="server.lua (ring a dispatcher first)"
-- With a dispatcher on shift the alarm rings at the console and the dispatcher
-- decides; otherwise the incident is created and auto-assigned.
local function storeAlarm(store)
    local dispatch = exports['koja-dispatcher']
    if dispatch:GetDispatcherCount() > 0 then
        return dispatch:CreateCall({
            coords = store.coords,
            label = store.label,
            caller = 'Alarm company',
            message = ('Silent alarm at %s'):format(store.label),
            category = 'robbery',
            priority = 1,
            need = { police = 2 },
        })
    end
    return dispatch:CreateIncident({
        category = 'robbery', priority = 1, coords = store.coords, label = store.label,
        need = { police = 2 }, note = 'Silent alarm', autoAssign = true,
    })
end
```

```lua title="client.lua"
-- in your robbery script, when the register is opened
TriggerServerEvent('myrobbery:server:alarm', 'ltd_grove')
```
::::

---

## ALPR camera script

Fixed cameras on the server read every vehicle that passes and report the plate. `ReportPlate` only alerts the dispatchers when the plate has an active BOLO, so you can report every read.

```lua title="server.lua"
local cameras = {
    { label = 'Camera 14, Del Perro Fwy', coords = vector3(-1236.0, -722.0, 21.0), radius = 25.0 },
    { label = 'Camera 3, Route 68', coords = vector3(1172.0, 2696.0, 37.9), radius = 25.0 },
}
local lastRead = {}

CreateThread(function()
    while true do
        Wait(1000)
        local now = os.time()
        for _, vehicle in ipairs(GetAllVehicles()) do
            local pos = GetEntityCoords(vehicle)
            for index, camera in ipairs(cameras) do
                if #(pos - camera.coords) <= camera.radius then
                    local plate = GetVehicleNumberPlateText(vehicle)
                    local key = index .. ':' .. tostring(plate)
                    if plate and (not lastRead[key] or now - lastRead[key] > 60) then
                        lastRead[key] = now
                        local hits = exports['koja-dispatcher']:ReportPlate(plate, pos, camera.label)
                        if #hits > 0 then
                            print(('[alpr] %s hit BOLO %s (%s)'):format(plate, hits[1].id, hits[1].title))
                        end
                    end
                end
            end
        end
    end
end)
```

A hit puts an ALPR alert and a radio line in every dispatcher's CAD and moves the BOLO's last-seen point to the camera. It needs `mdt_tools.alpr` on.

---

## Priority 1 for Fleeca robberies

An `incidentCreate` hook upgrades every robbery near a Fleeca branch, whether it came from a dispatcher, a robbery script, a phone or a dispatch-script alert.

```lua title="server.lua"
local fleeca = {
    vector3(149.9, -1040.7, 29.4),    -- Legion Square
    vector3(314.2, -278.6, 54.2),     -- Hawick Ave
    vector3(-351.5, -49.5, 49.0),     -- Burton
    vector3(-1212.9, -330.8, 37.8),   -- Rockford Hills
    vector3(-2962.6, 482.6, 15.7),    -- Great Ocean Hwy
    vector3(1175.0, 2706.6, 38.1),    -- Route 68
}

local function nearFleeca(coords)
    if not coords then return false end
    local point = vector3(coords.x, coords.y, coords.z or 0.0)
    for _, branch in ipairs(fleeca) do
        if #(point.xy - branch.xy) < 60.0 then return true end
    end
    return false
end

exports['koja-dispatcher']:RegisterHook('incidentCreate', function(data)
    if data.training or data.category ~= 'robbery' then return end
    local byLabel = type(data.label) == 'string' and data.label:lower():find('fleeca') ~= nil
    if byLabel or nearFleeca(data.real or data.coords) then
        return {
            priority = 1,
            need = { police = 3 },
            known = { police = 3 },
            note = 'Fleeca bank robbery: priority 1, three units minimum',
        }
    end
end)
```

`data.real` holds the true position when the dispatcher dispatched a call before learning the exact location.

---

## Block shifts for suspended players

A `shiftStart` hook refuses the shift with a reason. The CAD shows the locale text `errors.<reason>`, so add a `suspended` entry under `game.errors` in the locale files (or return an existing key such as `not_dispatcher`). Suspensions are kept in the resource KVP so they survive restarts.

::::codegroup
```lua title="server.lua"
local function licenseOf(src)
    return GetPlayerIdentifierByType(src, 'license')
end

local function suspendedUntil(license)
    return GetResourceKvpInt(('suspended:%s'):format(license))
end

exports['koja-dispatcher']:RegisterHook('shiftStart', function(data)
    local license = licenseOf(data.source)
    if license and suspendedUntil(license) > os.time() then
        return false, 'suspended'
    end
end)

-- /dispatchsuspend <id> <hours>, restricted with: add_ace group.admin command.dispatchsuspend allow
RegisterCommand('dispatchsuspend', function(src, args)
    local target, hours = tonumber(args[1]), tonumber(args[2])
    local license = target and licenseOf(target)
    if not license or not hours then return end
    SetResourceKvpInt(('suspended:%s'):format(license), os.time() + math.floor(hours * 3600))
    print(('suspended %s from dispatch for %sh'):format(GetPlayerName(target), hours))
end, true)
```

```json title="koja-dispatcher/locales/en.json (game.errors)"
"suspended": "You are suspended from dispatch duty."
```
::::

The hook only stops new shifts. To remove a dispatcher who is already on shift, end it in your own code (for example by kicking them from the job in your framework).

---

## Discord or log on incidentEnded

::::codegroup
```lua title="Dispatcher webhook"
-- uses the dispatcher's own webhooks (convar koja_dispatcher_webhook_incidents)
AddEventHandler('koja-dispatcher:incidentEnded', function(incident)
    if incident.status ~= 'resolved' or incident.priority ~= 1 then return end
    exports['koja-dispatcher']:DiscordLog('incidents',
        ('Priority 1 #%d closed'):format(incident.id),
        incident.note,
        {
            { name = 'Category', value = incident.category },
            { name = 'Outcome', value = incident.outcome or 'resolved' },
            { name = 'Location', value = incident.label or ('%.0f, %.0f'):format(incident.x, incident.y), inline = false },
            { name = 'Duration', value = ('%ds'):format((incident.endedAt or os.time()) - incident.createdAt) },
            { name = 'Dispatchers on shift', value = tostring(exports['koja-dispatcher']:GetDispatcherCount()) },
        },
        0x57F287)
end)
```

```lua title="Own webhook"
local webhook = GetConvar('my_incident_webhook', '')

AddEventHandler('koja-dispatcher:incidentEnded', function(incident)
    if webhook == '' then return end
    local body = json.encode({
        username = 'Incident log',
        embeds = { {
            title = ('Incident #%d %s'):format(incident.id, incident.status),
            description = ('%s, priority %d, outcome %s'):format(incident.category, incident.priority, tostring(incident.outcome)),
            color = incident.status == 'resolved' and 5763719 or 15548997,
        } },
    })
    PerformHttpRequest(webhook, function() end, 'POST', body, { ['Content-Type'] = 'application/json' })
end)
```

```lua title="Database log"
AddEventHandler('koja-dispatcher:incidentEnded', function(incident)
    MySQL.insert('INSERT INTO my_incident_log (incident, category, status, outcome, origin, ended_at) VALUES (?, ?, ?, ?, ?, ?)', {
        incident.id, incident.category, incident.status, incident.outcome, incident.origin, incident.endedAt,
    })
end)
```
::::

The dispatcher already stores every closed incident in `koja_dispatch_history` when `stats.enabled` is on; use your own table only for extra data.

---

## Field unit status from a job script

A booking or evidence script marks the officer busy while they work and available afterwards. `RunFieldCode` behaves like the officer's own key (radio line, HUD chip, koja-mdt sync); `SetUnitStatus` is the plain status change.

::::codegroup
```lua title="RunFieldCode"
-- server.lua of your booking script
RegisterNetEvent('mybooking:server:start', function()
    local src = source
    local ok, err = exports['koja-dispatcher']:RunFieldCode(src, '10-6')   -- busy
    if not ok and err ~= 'not_field_unit' then print('status failed: ' .. err) end
end)

RegisterNetEvent('mybooking:server:finish', function()
    exports['koja-dispatcher']:RunFieldCode(source, '10-8')                -- available
end)
```

```lua title="SetUnitStatus"
RegisterNetEvent('mybooking:server:start', function()
    local src = source
    local unit = exports['koja-dispatcher']:GetPlayerUnit(src)
    if not unit then return end                 -- not a field unit right now
    if unit.incident then return end            -- keep officers that are on a call
    exports['koja-dispatcher']:SetUnitStatus(src, 'busy')
end)

RegisterNetEvent('mybooking:server:finish', function()
    exports['koja-dispatcher']:SetUnitStatus(source, 'available')
end)
```
::::

:::hint{type="warning" title="Available releases the unit"}
`'available'` (and `10-8`) detaches the officer from their current incident. Check `unit.incident` first if the officer should stay on it.
:::

---

## Units on a custom map

The server sends a compact unit list every 2 seconds to the players who use your map (here: an ACE permission), and the client draws blips.

::::codegroup
```lua title="server.lua"
local viewers = {}

RegisterNetEvent('mymap:server:open', function()
    local src = source
    if IsPlayerAceAllowed(src, 'mymap.view') then viewers[src] = true end
end)

RegisterNetEvent('mymap:server:close', function() viewers[source] = nil end)
AddEventHandler('playerDropped', function() viewers[source] = nil end)

CreateThread(function()
    while true do
        Wait(2000)
        if next(viewers) then
            local list = {}
            for _, unit in ipairs(exports['koja-dispatcher']:GetUnits()) do
                if unit.status ~= 'offline' then
                    list[#list + 1] = {
                        id = unit.id, callsign = unit.callsign, dept = unit.dept,
                        status = unit.status, x = unit.x, y = unit.y, h = unit.h,
                        incident = unit.incident,
                    }
                end
            end
            for src in pairs(viewers) do TriggerClientEvent('mymap:client:units', src, list) end
        end
    end
end)
```

```lua title="client.lua"
local blips = {}
local colours = { police = 3, ems = 1, fire = 17 }

RegisterNetEvent('mymap:client:units', function(list)
    local seen = {}
    for _, unit in ipairs(list) do
        seen[unit.id] = true
        local blip = blips[unit.id]
        if not blip then
            blip = AddBlipForCoord(unit.x, unit.y, 0.0)
            SetBlipSprite(blip, unit.dept == 'police' and 56 or 61)
            SetBlipColour(blip, colours[unit.dept] or 0)
            SetBlipScale(blip, 0.8)
            BeginTextCommandSetBlipName('STRING')
            AddTextComponentSubstringPlayerName(unit.callsign)
            EndTextCommandSetBlipName(blip)
            blips[unit.id] = blip
        end
        SetBlipCoords(blip, unit.x, unit.y, 0.0)
        SetBlipRotation(blip, math.floor(unit.h or 0))
        SetBlipFlashes(blip, unit.status == 'enroute')
    end
    for id, blip in pairs(blips) do
        if not seen[id] then RemoveBlip(blip); blips[id] = nil end
    end
end)
```
::::

For a log of who is on duty, listen to `koja-dispatcher:unitAdded` and `koja-dispatcher:unitRemoved` instead of polling.

---

## Create a BOLO from another script

`CreateBolo` creates the BOLO where the CAD keeps them (`mdt_tools.boloSource`): in koja-mdt while it runs, otherwise in the dispatcher's own list. Every dispatcher sees it at once, and with koja-mdt officers see it in the MDT too.

```lua title="server.lua"
-- a bank robbery script flags the getaway car
RegisterNetEvent('mybank:server:getaway', function(netId)
    local src = source
    local vehicle = NetworkGetEntityFromNetworkId(netId)
    if vehicle == 0 or #(GetEntityCoords(vehicle) - GetEntityCoords(GetPlayerPed(src))) > 15.0 then return end

    local pos = GetEntityCoords(vehicle)
    local boloId, err = exports['koja-dispatcher']:CreateBolo({
        type = 'vehicle',
        title = 'Getaway car, armed occupants',
        plate = GetVehicleNumberPlateText(vehicle),
        description = 'Fled the Pacific Standard robbery',
        priority = 1,
        point = { x = pos.x, y = pos.y },
        label = 'Pacific Standard Bank',
    }, 'Pacific Standard security')

    if not boloId then
        print('[mybank] BOLO failed: ' .. err)   -- e.g. plate_required, mdt_offline
        return
    end

    -- close it automatically after 30 minutes
    SetTimeout(30 * 60 * 1000, function()
        exports['koja-dispatcher']:CloseBolo(boloId)
    end)
end)
```

| Result | Meaning |
|---|---|
| `'m12'` | Created in koja-mdt as BOLO 12 (also visible in the MDT). |
| `'i7'` | Created in the dispatcher's own list; on-duty police units get a notification (`mdt_tools.notifyUnits`) and `koja-dispatcher:boloCreated` fires. |
| `nil, 'mdt_offline'` | `mdt_tools.boloSource = 'mdt'` and koja-mdt is not running. |

`GetBolos()` returns the list as the CAD sees it about 0.3 s after the change. All fields and errors are on the [server exports](/new/dispatcher/api/server-exports) page.

---

## Escalation in a custom scenario

Generated calls come from `Scenarios.List`. Add your own in a new file in `shared/scenarios/` (loaded by the manifest, not escrowed). This one starts as a burglary, escalates while the caller is on the line, and continues on scene in stages.

```lua title="koja-dispatcher/shared/scenarios/my_scenarios.lua"
local list = {
    jewelry_after_hours = {
        category = 'burglary',
        priority = 2,
        weight = 2,                          -- how often it is picked compared to others
        tags = { 'jewelry', 'store' },       -- location tags from shared/locations.lua
        distress = 0.5,
        time = { 'night' },                  -- more likely at night
        opening = {
            "I'm the night guard at the jewelry store, the back door alarm just went off.",
            "Someone is inside the jewelry store, I can see flashlights through the window!",
        },
        answers = {
            what = 'Back door alarm, then I saw two flashlights moving behind the counters.',
            suspects = { chance = 0.6, text = 'At least three of them, masks on.', reveal = { police = 1 }, alt = 'I only saw one.' },
            weapons = { chance = 0.4, text = 'One of them has a shotgun!', reveal = { police = 1 } },
        },
        base = { police = 1 },
        instruction = 'hide',
        respond = 150,
        fail = 360,
        work = 60,
        points = 140,
        scene = 'crowd',
        escalate = { after = 180, add = { police = 1 }, text = 'Suspects are loading a van out back!' },

        events = {
            -- 20 s after the dispatcher answered: the situation changes on the line
            {
                at = 20,
                line = "They smashed the display cases, they're grabbing everything!",
                radio = 'Caller reports the suspects are smashing display cases.',
                distress = 0.2,
                priority = 1,
                known = { police = 1 },
                alert = 'escalation',
            },
            -- only when the dispatcher asks about weapons, with a 2 s delay
            {
                on = 'ask:weapons',
                chance = 0.5,
                delay = 2,
                line = 'Oh no, one of them just fired at the ceiling!',
                category = 'shooting',               -- coding the call as shooting also counts as correct
                need = { ems = 1 },
                alert = 'shots',
            },
            -- 45 s after dispatch: a callback reaches the incident
            {
                after = 45,
                when = 'incident',
                line = 'A neighbour called back: the van is leaving north on Palomino!',
                radio = 'Callback: suspect van leaving north on Palomino Ave.',
                need = { police = 1 },
                priority = 1,
            },
        },

        stages = {
            { name = 'Burglary in progress', note = 'Suspects inside the store.' },
            {
                name = 'Injured guard',
                category = 'medical',
                need = { ems = 1, police = 1 },
                work = 40,
                fail = 240,
                transport = true,
                points = 40,
                radio = 'Store secured. The night guard was assaulted and needs EMS.',
                note = 'Guard with a head wound behind the counter.',
            },
        },
    },
}

for key, scenario in pairs(list) do
    if Scenarios.List[key] == nil then Scenarios.List[key] = scenario end
end
```

| Event field | Meaning |
|---|---|
| `at` | Seconds after the call was answered. If the call was dispatched already, it fires on the incident. |
| `on` | `ask:<question>`, `ask`, `instruct:<option>`, `instruct`, `instruct:good`, `instruct:bad`. Only while the caller is on the line. |
| `after` | Seconds after dispatch, on the incident only. |
| `when` | `'call'` or `'incident'` limits where it may fire. |
| `chance` | Rolled once per call. |
| `delay` | Seconds between the trigger and the event (default `Config.Dynamics.eventDelay`, 2). |
| `line` | Caller line (string or list, `{location}` works). |
| `radio`, `alert`, `ai`, `note` | Radio text, CAD alert kind, extra context for AI callers, incident note. |
| `distress`, `priority`, `category` | Added distress, raised priority, category suggestion. |
| `need` / `known` | Hidden extra units / extra units the dispatcher learns about. |
| `silent` | The caller stops answering. |

| Stage field | Meaning |
|---|---|
| `name` | Stage name in the CAD. Stage 1 is the incident as created. |
| `need` / `add` / `hidden` | Replace the need / add to it / add units revealed only on scene. |
| `work`, `fail`, `points`, `priority`, `category`, `transport`, `escalate`, `note`, `radio` | Values for the new stage. |

Each next stage starts when progress reaches 100 %; units stay on scene and missing units are requested over the radio. `dynamics.events` and `dynamics.stages` must be on.

Test it right away:

::::codegroup
```text title="Server console"
dispatchtest jewelry_after_hours
```

```lua title="server.lua"
exports['koja-dispatcher']:CreateCall({
    scenario = 'jewelry_after_hours',
    coords = vector3(-630.4, -236.7, 38.1),
    label = 'Vangelico, Rockford Hills',
})
```
::::

Scenarios can also be made in the in-game scenario editor in `/dispatchadmin`, which stores them in the database.

---

## Register your own admin setting

`RegisterSetting` adds your own keys to the dispatcher's admin menu (Modules page). Admins change them in `/dispatchadmin`, the values are saved with the other settings and survive restarts, and every resource can read them with `GetSetting`.

::::steps
### Register the keys

```lua title="your-resource/server.lua"
local dispatch = exports['koja-dispatcher']

local function registerSettings()
    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' } })
    dispatch:RegisterSetting('myserver.cooldown', 'number', 10, { min = 1, max = 60, step = 0.5 })
    -- client = true also sends the value to every client
    dispatch:RegisterSetting('myserver.showHint', 'bool', false, { client = true })
end

-- on your resource start (also after a restart: re-registering returns true
-- and attaches the new onChange handler)
registerSettings()

-- and again when koja-dispatcher restarts after your resource
AddEventHandler('onResourceStart', function(name)
    if name == 'koja-dispatcher' then registerSettings() end
end)
```

The group is the part before the first dot (`myserver`) unless you pass `group`. Saved values are restored even though your resource registers the keys after koja-dispatcher loaded its settings. On a re-register only `onChange` changes; type, range, default and group stay as first registered until koja-dispatcher restarts.

### Add the labels

Add the group title and the labels to `koja-dispatcher/locales/en.json` (and your other locales) under `ui.admin`:

```json title="locales/en.json"
{
  "ui": {
    "admin": {
      "module": {
        "myserver": "My server"
      },
      "set": {
        "myserver": {
          "robberyAlerts": "Robbery alerts",
          "robberyAlerts_hint": "Store robberies alert the dispatcher.",
          "robberyUnits": "Robbery units",
          "robberyUnits_hint": "Police units sent to a store robbery.",
          "robberyRoute": "Robbery alerts become",
          "robberyRoute_hint": "call rings at the console, incident goes straight to the map.",
          "cooldown": "Robbery cooldown (min)",
          "cooldown_hint": "Minutes between robberies of the same store.",
          "showHint": "Show robbery hint",
          "showHint_hint": "Shows a hint to robbers."
        }
      }
    }
  }
}
```

Merge these keys into the existing objects; do not replace the file.

### Read the values

```lua title="your-resource/server.lua"
local function robberyAlert(coords)
    if not dispatch:GetSetting('myserver.robberyAlerts') then return end
    local units = dispatch:GetSetting('myserver.robberyUnits')
    if dispatch:GetSetting('myserver.robberyRoute') == 'incident' then
        dispatch:CreateIncident({ category = 'robbery', coords = coords, need = { police = units }, autoAssign = true })
    else
        dispatch:CreateCall({ coords = coords, category = 'robbery', need = { police = units }, message = 'Store robbery' })
    end
end

-- other resources can react to the same key with the event
AddEventHandler('koja-dispatcher:settingChanged', function(change)
    if change.key == 'myserver.robberyAlerts' then print('robbery alerts: ' .. tostring(change.value)) end
end)
```
::::

:::hint{type="info" title="Inside koja-dispatcher"}
If you prefer to keep the setting with the resource, register it in a file in `server/custom/` (loaded automatically after the built-in modules) with `KOJA.Server.Settings.Register(key, KOJA.Server.Settings.Int(2, 1, 8), { group = 'myserver' })`. Such a key cannot be re-registered with `RegisterSetting` (it returns `false`). Keep your `server/custom/` files when you update.
:::
