Koja Scripts

Integrations

How to plug a custom jail, fine, impound, phone, dispatch or framework system into koja-mdt through the editable bridges, exports and hooks.

Integrations

koja-mdt talks to the rest of your server in three ways:

WayWhereUse it for
Editable bridgeseditable/ (not escrowed)Replace how the MDT jails, fines, impounds, bills, reads framework data or reads the phone number.
Exportsserver / clientLet your scripts create calls, citations, warrants, report sentences, read records.
Hooks and eventshooks / eventsValidate or mirror MDT actions in your own systems.

Editable files

FileSideContents
editable/server/framework.luaserverBridge: citizen, vehicle, license, roster and offline job queries for ESX (Esx), QBCore / Qbox (Qb) and a Custom stub. The active implementation follows KojaLib.Framework.
editable/server/integrations.luaserverIntegrations: jail and impound adapters, fines, society payments, medical billing, citizen photos and the integration events.
editable/client/integrations.luaclientClientIntegrations: death check, open conditions, phone number, notifications.

Jail systems

When an officer processes a suspect (or a judge applies a sentence) the MDT calls Integrations.Jail(target, months, reason, officerSrc, context). It picks an adapter based on Config.Processing.jailSystem:

ValueBehaviour
'auto'The first started resource in the adapter list: koja-police, pickle_prisons, rcore_prison, xt-prison, qb-prison, esx_jail.
'<resource>'Only that adapter.
'none'No game jail; the MDT only records the sentence (custody tab, release timer).

When no adapter handles the request the MDT fires koja-mdt:server:jailPlayer with target, months, reason, officerSrc and still records the sentence.

Add an entry to jailAdapters in editable/server/integrations.lua. jail must return false on failure; release and isJailed are optional (early release from the MDT and automatic release tracking use them).

editable/server/integrations.lua
{
    resource = 'my-prison',
    jail = function(target, months, reason)
        return exports['my-prison']:Jail(target, months, reason) ~= false
    end,
    release = function(target, identifier, reason)
        return exports['my-prison']:Release(identifier) ~= false
    end,
    isJailed = function(target)
        return exports['my-prison']:IsJailed(target) == true
    end,
},

Set Config.Processing.jailSystem = 'my-prison' or keep 'auto'.

Leave the adapters alone and listen to the fallback event. The MDT already records the sentence; report the end of it.

my-prison/server.lua
AddEventHandler('koja-mdt:server:jailPlayer', function(target, months, reason)
    StartSentence(target, months, reason)
end)

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

function OnSentenceEnded(src, escaped)
    exports['koja-mdt']:ReportRelease(src, escaped and 'escaped' or 'released', { by = 'My Prison' })
end

Your prison script jails people on its own (for example a judge NPC). Report what happened so the MDT custody tab, criminal record and timers stay correct.

my-prison/server.lua
exports['koja-mdt']:ReportJail(src, months, { reason = reason, facility = 'Bolingbroke', officer = officerSrc })
exports['koja-mdt']:ReportRelease(src, 'released', { by = 'Bolingbroke' })

The record shows the adapter in system. Every 30 seconds the MDT closes records whose time is up (Config.Custody.autoRelease, skipped for systems listed in Config.Custody.trackedSystems), and for adapters with isJailed it also closes records of online inmates the adapter no longer holds.

Fines and money

Processing, citations paid in game and impound fees all call Integrations.Fine(target, amount, reason, officerSrc). The default implementation:

  • takes the money from Config.Processing.fineAccount (default bank), falling back to cash when the account is short;
  • allows a negative balance when Config.Processing.allowNegativeBalance = true;
  • pays the amount to Config.Processing.fineSociety through Integrations.PaySociety (esx_addonaccount, Renewed-Banking, qb-banking or qb-management).

Replace the function body to use your own banking. It must return true only when the money was taken.

editable/server/integrations.lua
function Integrations.Fine(target, amount, reason, officerSrc)
    if amount <= 0 then return true end
    local ok = exports['my-bank']:Charge(target, amount, reason)
    if ok then exports['my-bank']:AddToSociety('police', amount) end
    return ok == true
end

Medical bills use Integrations.ChargeMedicalBill(target, amount, reason, medicSrc, bill) and Integrations.PayMedicalSociety(amount) (Config.Ems.billing.account and Config.Ems.billing.society). When koja-ambulance is running with billing = 'ambulance' the bill is sent as an ambulance invoice instead.

If your bank charges citizens itself, mark citations paid with the export:

server.lua
exports['koja-mdt']:PayCitation(citationId, { author = 'Maze Bank' })

Impound and garages

MDT impounds call Integrations.Impound(plate, data) with Config.Custody.impoundSystem:

ValueBehaviour
'auto'The first available adapter: koja-police, then garage (sets the stored / state column of owned_vehicles or player_vehicles).
'<adapter id>'Only that adapter.
'none' / 'mdt'No garage change; the MDT only keeps the impound record.

Every MDT impound also fires koja-mdt:server:impoundVehicle and every MDT release koja-mdt:server:releaseImpound. To add your garage, add an entry to impoundAdapters:

editable/server/integrations.lua
{
    id = 'my-garage',
    resource = 'my-garage',
    impound = function(plate, data)
        return exports['my-garage']:SetImpounded(plate, true, data.fee) ~= false
    end,
    release = function(plate, data)
        return exports['my-garage']:SetImpounded(plate, false) ~= false
    end,
},

A garage or tow script that impounds on its own reports it, and checks the MDT before handing a car out:

my-garage/server.lua
exports['koja-mdt']:ReportImpound(plate, { reason = 'Abandoned', fee = 750, hours = 12, lot = 'davis' })
if exports['koja-mdt']:IsImpounded(plate) then return end

Phones

NeedUse
Caller number on 911 callsClientIntegrations.GetPhoneNumber() in editable/client/integrations.lua. Return the player's number from your phone resource.
911 / 311 from a phone appClient SendDispatch({ code = '911', message = text }), or server CreateDispatch when your phone already runs server side.
Warrant or citation appServer exports GetCitizenWarrants, HasActiveWarrant, GetUnpaidCitations, GetOutstandingTotal, PayCitation.
MDT inbox badgeGetUnreadMailCount(src) and the client koja-mdt:client:notification event.
editable/client/integrations.lua
function ClientIntegrations.GetPhoneNumber()
    return exports['my-phone']:GetNumber()
end

Other dispatch systems

GoalHow
Feed calls from another script into the MDTCreateDispatch(data), optionally with your own codes from RegisterDispatchCode.
Mirror MDT calls to another dispatch, a Discord channel or a CADListen to koja-mdt:dispatch:create, koja-mdt:dispatch:close, koja-mdt:dispatch:assign or register after hooks.
Filter or reroute callsA dispatch:create before hook can change departments, priority, title or cancel the call.
Use another dispatch onlySet Config.Dispatch.enabled = false; CreateDispatch then returns nil.
server.lua
exports['koja-mdt']:registerHook('dispatch:create', function(call)
    exports['other-cad']:PushCall({ id = call.id, code = call.code, title = call.title, coords = call.coords })
end)

koja-police and koja-ambulance are detected automatically (Config.Police, Config.Ems.ambulance): their dispatch, duty and billing are linked without code.

Frameworks

ESX, QBCore and Qbox work out of the box. When KojaLib.Framework is anything else, the MDT uses the Custom table in editable/server/framework.lua. Fill it for your framework:

FunctionReturns
SearchCitizens(query, limit){ identifier, firstname, lastname, dob, gender, phone }[]
GetCitizen(identifier)one citizen or nil
GetCitizensByIdentifiers(list){ [identifier] = 'Full Name' }
SearchVehicles(query, limit) / GetVehicle(plate) / GetVehiclesByPlates(plates){ plate, model, owner, ownerName }
GetCitizenVehicles(identifier)vehicles of the citizen
GetLicenses(identifier) / GrantLicense(identifier, type) / RevokeLicense(identifier, type)license list / boolean
GetRoster(jobs){ identifier, name, job, grade }[] of every member of the jobs
GetOfflineJob(identifier){ name, grade, fullname } or nil

Bridge.ChargeOffline(identifier, amount, account, reason) is optional: when present, citations can be paid from the web portal while the citizen is offline.

Integrations.CitizenImage(identifier) can return a photo URL for citizens that have no MDT photo (for example from your character creator).

koja-lib

koja-mdt requires koja-lib and uses it for everything framework specific: player identifiers, jobs and grades (KojaLib.Server.GetPlayerIdentifier, GetJobInfo, GetJobs), money (getMoney, removeMoney), job changes (SetJob, SetOfflineJob), notifications (KojaLib.Server.SendNotify, KojaLib.Client.SendNotify) and Discord webhooks (LogMessage). KojaLib.Framework (esx, qb, custom) selects the bridge. The same helpers are available in the editable files, so a custom adapter does not need to know the framework:

editable/server/integrations.lua
function Integrations.CitizenImage(identifier)
    local src = KojaLib.Server.GetSourceByIdentifier(identifier)
    return src and exports['my-mugshots']:Get(src) or nil
end