Koja Scripts

API examples

Complete koja-mdt integration recipes: robbery alerts, speed cameras, prison scripts, phone apps, garages and hook rules.

Examples

Complete recipes built only from the documented server exports, client exports, hooks and events.

Robbery script creating a dispatch call

  1. Register the call type

    my-robberies/server.lua
    local mdt = exports['koja-mdt']
    
    CreateThread(function()
        mdt:RegisterDispatchCode('ROB-STORE', {
            title = 'Store robbery', priority = 1, types = { 'leo' },
            blip = { sprite = 52, color = 1, scale = 1.1 },
        })
    end)
  2. Create the call when a validated robbery starts

    my-robberies/server.lua
    local active = {}
    
    RegisterNetEvent('my-robberies:server:start', function(storeId)
        local src = source
        local store = Config.Stores[storeId]
        if not store or active[storeId] then return end
        if #(GetEntityCoords(GetPlayerPed(src)) - store.coords) > 15.0 then return end
        if mdt:CountOnDuty({ type = 'leo' }) < store.minPolice then return end
        active[storeId] = mdt:CreateDispatch({
            code = 'ROB-STORE',
            message = ('Silent alarm at %s'):format(store.label),
            coords = store.coords,
            caller = { name = 'Alarm company' },
            source = src,
        })
    end)
  3. Update and close it

    my-robberies/server.lua
    RegisterNetEvent('my-robberies:server:safeOpened', function(storeId)
        local callId = active[storeId]
        if not callId then return end
        mdt:UpdateDispatch(callId, { message = 'Safe opened, suspects leaving' })
        mdt:AddDispatchNote(callId, 'Camera shows 2 suspects, masks', 'Store CCTV')
    end)
    
    RegisterNetEvent('my-robberies:server:finished', function(storeId)
        local callId = active[storeId]
        active[storeId] = nil
        SetTimeout(10 * 60 * 1000, function()
            local call = callId and mdt:GetCall(callId)
            if call and call.status == 'active' and #call.units == 0 then mdt:CloseDispatch(callId) end
        end)
    end)

Speed camera issuing citations

my-speedcams/server.lua
local mdt = exports['koja-mdt']
local flashed = {}

RegisterNetEvent('my-speedcams:server:flash', function(cameraId, plate, speed)
    local src = source
    local camera = Config.Cameras[cameraId]
    if not camera or type(plate) ~= 'string' or type(speed) ~= 'number' then return end
    if #(GetEntityCoords(GetPlayerPed(src)) - camera.coords) > 60.0 then return end
    local key = plate .. ':' .. cameraId
    if flashed[key] and os.time() - flashed[key] < 60 then return end
    flashed[key] = os.time()

    local vehicle = mdt:GetVehicle(plate)
    mdt:ReportAnprRead({ plate = plate, cameraLabel = camera.label, coords = camera.coords, department = 'sahp' })
    if not vehicle or vehicle.unregistered then return end

    local citation, err = mdt:IssueCitation({
        identifier = vehicle.owner,
        plate = vehicle.plate,
        vehicleModel = vehicle.model,
        violations = { speed - camera.limit > 35 and 'V.C. 7002' or 'V.C. 7001' },
        location = camera.label,
        coords = camera.coords,
        notes = ('Speed camera: %d in a %d zone'):format(math.floor(speed), camera.limit),
        department = 'sahp',
        author = 'Speed Camera ' .. cameraId,
    })
    if not citation then print('citation failed: ' .. tostring(err)) return end

    mdt:UpdateVehicle(vehicle.plate, { points = (vehicle.points or 0) + 1 })
end)

ReportAnprRead logs the plate only when it is a hit (stolen, BOLO, warrant), and with Config.Anpr.autoDispatch raises a call for SAHP. The citation reaches the owner's notification center and citation list; license points are applied by the MDT.

Jail script reporting sentences

my-prison/server.lua
local mdt = exports['koja-mdt']

AddEventHandler('koja-mdt:server:jailPlayer', function(target, months, reason, officerSrc)
    StartSentence(target, months, reason)
end)

AddEventHandler('koja-mdt:server:releasePlayer', function(target, identifier, reason)
    if target then EndSentence(target) end
end)

function JailFromJudgeNpc(src, months, reason)
    StartSentence(src, months, reason)
    mdt:ReportJail(src, months, { reason = reason, facility = 'Bolingbroke', officer = 'Judge NPC' })
end

function OnSentenceServed(src)
    mdt:ReportRelease(src, 'released', { by = 'Bolingbroke' })
end

function OnEscape(src)
    mdt:ReportRelease(src, 'escaped', { by = 'Bolingbroke' })
    local coords = GetEntityCoords(GetPlayerPed(src))
    mdt:CreateDispatch({ code = '10-98', title = 'Prison escape', coords = coords, priority = 1, types = { 'leo' } })
end

RegisterNetEvent('my-prison:server:remaining', function()
    local src = source
    local sentence = mdt:GetActiveSentence(src)
    TriggerClientEvent('my-prison:client:remaining', src, sentence and sentence.secondsLeft or 0)
end)

Sentences from MDT processing already create the jail record; only report the ones your script starts on its own, and always report the end of a sentence. See integrations for writing a jail adapter instead.

Phone app reading warrants and citations

my-phone/server.lua
local mdt = exports['koja-mdt']
local lastRequest = {}

RegisterNetEvent('my-phone:server:records', function()
    local src = source
    if lastRequest[src] and os.time() - lastRequest[src] < 5 then return end
    lastRequest[src] = os.time()

    local warrants = {}
    for _, warrant in ipairs(mdt:GetCitizenWarrants(src)) do
        if warrant.status == 'active' then
            warrants[#warrants + 1] = { id = warrant.id, type = warrant.type, reason = warrant.reason, expiresAt = warrant.expiresAt }
        end
    end
    local owed, count = mdt:GetOutstandingTotal(src)

    TriggerClientEvent('my-phone:client:records', src, {
        warrants = warrants,
        citations = mdt:GetUnpaidCitations(src),
        owed = owed,
        openCitations = count,
    })
end)

RegisterNetEvent('my-phone:server:payCitation', function(citationId)
    local src = source
    if type(citationId) ~= 'number' then return end
    local own
    for _, citation in ipairs(mdt:GetUnpaidCitations(src)) do
        if citation.id == citationId then own = citation end
    end
    if not own then return end
    local paid, err = mdt:PayCitation(own.id, { charge = true })
    TriggerClientEvent('my-phone:client:paid', src, paid ~= nil, err)
end)

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

PayCitation with charge = true takes the money through the MDT fine integration. Only act on a player's own records: the pay handler only accepts a citation id from the caller's own unpaid list.

Garage checking impounds

my-garage/server.lua
local mdt = exports['koja-mdt']

RegisterNetEvent('my-garage:server:takeOut', function(plate)
    local src = source
    if type(plate) ~= 'string' then return end
    local held = mdt:GetHeldImpound(plate)
    if held then
        TriggerClientEvent('my-garage:client:notify', src, ('Impounded at %s. Fee $%d.'):format(held.lot, held.fee))
        return
    end
    if mdt:HasVehicleFlag(plate, 'stolen') then
        TriggerClientEvent('my-garage:client:notify', src, 'This vehicle is reported stolen.')
        return
    end
    SpawnVehicle(src, plate)
end)

RegisterNetEvent('my-garage:server:payImpound', function(plate)
    local src = source
    local held = type(plate) == 'string' and mdt:GetHeldImpound(plate)
    if not held or held.holdLeft > 0 then return end
    local record, err = mdt:ReleaseImpound(plate, { feeStatus = 'charge', note = 'Paid at impound lot' })
    TriggerClientEvent('my-garage:client:notify', src, record and 'Vehicle released' or ('Release failed: ' .. tostring(err)))
end)

AddEventHandler('my-tow:server:towed', function(plate, reason)
    mdt:ReportImpound(plate, { reason = reason, fee = 350, hours = 0, lot = 'davis' })
end)

ReleaseImpound with feeStatus = 'charge' takes the fee from the online owner; ReportImpound records a tow your script already performed.

Rules as hooks

my-rules/server.lua
local mdt = exports['koja-mdt']

mdt:registerHook('citation:create', function(payload)
    if payload.total > 5000 then return false, 'over_limit' end
end, { stage = 'before' })

mdt:registerHook('incident:process', function(payload)
    if payload.months > 60 then return { months = 60 } end
end, { stage = 'before' })

mdt:registerHook('mdt:open', function(payload)
    return exports.ox_inventory:Search(payload.source, 'count', 'mdt_tablet') > 0
end, { stage = 'before' })

AddEventHandler('koja-mdt:server:dutyChanged', function(src, onDuty, department)
    if src then exports['my-hud']:SetBadge(src, onDuty and department or nil) end
end)