otm_radio
Server-authoritative police radio for pma-voice. One resource, four targets: QBX, QBCore, ESX, standalone (auto-detected).
Features
- Encrypted frequency ranges per job / job type / grade / duty (LEO, Command, EMS, Joint by default)
- Every encrypted frequency is registered as a pma-voice channel check, so a modded client triggering
pma-voice:setPlayerRadiodirectly still can't get in - Server sweep (default 3s) removes anyone who loses the job, goes off duty, drops the radio item or goes down
- Unit list with callsigns + live talking indicators on every channel (open/civilian channels too;
Config.Ui.showPublicMembers = falsehides names there) - On-screen unit list overlay (
/radiolist, keybind, or radio Settings > Unit List): stays up with the radio put away; drag the header to move, the corner to resize (while the radio is open or via/radiolist edit); saved per player. Text-only style (no background): the square toggle in the list header,/radiolist bg, orConfig.Roster.background = falseas the default. - Resizable radio: Settings > Radio Size (left / right or -/+), saved per player;
Config.Ui.scaleis the starting size SeeConfig.Roster - Panic button (hold-to-trigger in UI,
/panic, keybind): server-side coords, cooldown, flashing blip + sound to on-duty units - Callsigns: QB/QBX read/write
metadata.callsign(stays in sync with PD scripts); ESX/standalone use oxmysql or KVP - Presets filtered by what the player can access; next/prev channel keybinds
- Hold / talk animations with radio prop, volume + mic-click prefs saved per client
- Rate limiting on every server callback, input sanitised server-side, all NUI text rendered with
textContent
Requirements
ox_lib,pma-voice(withsetr voice_enableRadios 1)- Optional:
oxmysql(callsign storage on ESX/standalone; KVP fallback otherwise) - Inventory: ox_inventory, qb-inventory, qs-inventory, ESX default, or none
Install
- Drop
otm_radiointo your resources folder. - Stop any other radio resource that drives pma-voice (
qbx_radio,qb-radio,mm_radio, etc.). QBX shipsqbx_radioby default. server.cfg, after pma-voice and your framework:ensure ox_lib ensure pma-voice ensure otm_radio- Add the item (below). Done — the SQL table auto-creates if oxmysql is running.
Item
ox_inventory (ox_inventory/data/items.lua):
['radio'] = {
label = 'Radio',
weight = 500,
stack = false,
close = true,
client = { export = 'otm_radio.useRadio' },
},
qb-core (qb-core/shared/items.lua):
radio = { name = 'radio', label = 'Radio', weight = 500, type = 'item', image = 'radio.png', unique = true, useable = true, shouldClose = true, description = 'Handheld radio' },
ESX: run the INSERT in sql/otm_radio.sql.
Don't want an item? Config.Item.required = false and use /radio or the keybind.
Standalone jobs (ACE)
add_ace group.police otm_radio.job.police allow
add_ace group.police_command otm_radio.job.police_command allow
add_ace group.ems otm_radio.job.ambulance allow
add_principal identifier.license:xxxx group.police
Config notes
- Restricted ranges must not overlap.
jobsmatches job name,jobTypesmatches QBX/QB job type (leo,ems), value = minimum grade. Frequency.decimals = 2locks 100 frequencies per whole number in a range (default ranges = 900 locks, registered once at start). If you widen ranges a lot, lower decimals to 1.- Death detection: QB/QBX use
metadata.isdead / inlaststand. ESX/standalone use theisDeadstatebag or 0 health. If your ambulance job uses something else, editstateDeadinbridge/server/framework.lua. - Keybinds default to unbound; players set them in Settings → Key Bindings → FiveM.
Exports
Server:
exports.otm_radio:getPlayerChannel(src) -- number (0 = none)
exports.otm_radio:getChannelMembers(freq) -- { id, name, callsign, talking }[]
exports.otm_radio:kickFromRadio(src, reason?) -- reason = locale key, e.g. 'removed'
exports.otm_radio:canJoinFrequency(src, freq) -- boolean
Client:
exports.otm_radio:openRadio()
exports.otm_radio:closeRadio()
exports.otm_radio:isOpen()
exports.otm_radio:isPowered()
exports.otm_radio:getFrequency()
Extending
- New framework: add a branch in
bridge/server/framework.lua(getRawPlayer,getPlayerData, events) andbridge/client/framework.lua(onUnload). - New inventory: add a branch in
bridge/server/inventory.lua→hasItem. - New language: copy
locales/en.luatolocales/xx.lua, changeLocales.entoLocales.xx, setConfig.Locale.