Integrations
koja-mdt talks to the rest of your server in three ways:
| Way | Where | Use it for |
|---|---|---|
| Editable bridges | editable/ (not escrowed) | Replace how the MDT jails, fines, impounds, bills, reads framework data or reads the phone number. |
| Exports | server / client | Let your scripts create calls, citations, warrants, report sentences, read records. |
| Hooks and events | hooks / events | Validate or mirror MDT actions in your own systems. |
Editable files
| File | Side | Contents |
|---|---|---|
editable/server/framework.lua | server | Bridge: 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.lua | server | Integrations: jail and impound adapters, fines, society payments, medical billing, citizen photos and the integration events. |
editable/client/integrations.lua | client | ClientIntegrations: 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:
| Value | Behaviour |
|---|---|
'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).
{
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.
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' })
endYour 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.
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(defaultbank), falling back to cash when the account is short; - allows a negative balance when
Config.Processing.allowNegativeBalance = true; - pays the amount to
Config.Processing.fineSocietythroughIntegrations.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.
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
endMedical 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:
exports['koja-mdt']:PayCitation(citationId, { author = 'Maze Bank' })Impound and garages
MDT impounds call Integrations.Impound(plate, data) with Config.Custody.impoundSystem:
| Value | Behaviour |
|---|---|
'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:
{
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:
exports['koja-mdt']:ReportImpound(plate, { reason = 'Abandoned', fee = 750, hours = 12, lot = 'davis' })
if exports['koja-mdt']:IsImpounded(plate) then return endPhones
| Need | Use |
|---|---|
| Caller number on 911 calls | ClientIntegrations.GetPhoneNumber() in editable/client/integrations.lua. Return the player's number from your phone resource. |
| 911 / 311 from a phone app | Client SendDispatch({ code = '911', message = text }), or server CreateDispatch when your phone already runs server side. |
| Warrant or citation app | Server exports GetCitizenWarrants, HasActiveWarrant, GetUnpaidCitations, GetOutstandingTotal, PayCitation. |
| MDT inbox badge | GetUnreadMailCount(src) and the client koja-mdt:client:notification event. |
function ClientIntegrations.GetPhoneNumber()
return exports['my-phone']:GetNumber()
endOther dispatch systems
| Goal | How |
|---|---|
| Feed calls from another script into the MDT | CreateDispatch(data), optionally with your own codes from RegisterDispatchCode. |
| Mirror MDT calls to another dispatch, a Discord channel or a CAD | Listen to koja-mdt:dispatch:create, koja-mdt:dispatch:close, koja-mdt:dispatch:assign or register after hooks. |
| Filter or reroute calls | A dispatch:create before hook can change departments, priority, title or cancel the call. |
| Use another dispatch only | Set Config.Dispatch.enabled = false; CreateDispatch then returns nil. |
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:
| Function | Returns |
|---|---|
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:
function Integrations.CitizenImage(identifier)
local src = KojaLib.Server.GetSourceByIdentifier(identifier)
return src and exports['my-mugshots']:Get(src) or nil
end