Skip to content

Configuration

Complete configuration reference for Tommy's Radio — config.lua, framework hooks, zones and channels, codeplugs, every admin panel setting, sounds, and integrations.

Where each setting lives

SourceContainsEdited inApplies
config/config.luaNetwork, auth, Discord, framework, inventoryText editorAfter restart tRadio
config/sv_functions.luaServer hooks — job→codeplug mapping, access, player names, 911 caller IDText editorAfter restart tRadio
config/cl_functions.luaClient hooks — canTalk, battery, background sound detection, animationsText editorAfter restart tRadio
data.jsonZones, channels, codeplugs, alerts, speakers, towers, geo zones, all server settingsWeb admin panelInstantly, no restart

The rule of thumb: a setting lives in a .lua file only if it is a genuine Lua function, or if applying it binds a port or registers a command that FiveM cannot undo. Everything else is in the admin panel at http://your-server-ip:port/admin.


config.lua

Server and authentication

Config = {
    serverAddress     = "http://203.0.113.10:7777",
    isSatelliteServer = false,
    serverPort        = 7777,
    authToken         = "<20+ random characters>",
    adminPassword     = "<your admin password>",
    dispatchNacId     = "4417",
}
KeyDefaultDescription
serverAddress"localhost"Public IP or domain of the voice server, including the port. "" auto-detects the public IP. Behind a reverse proxy, use the full URL: "https://radio.yourdomain.com"
isSatelliteServerfalsetrue on secondary game servers that federate into one shared host — see Multi-server federation
serverPort7777TCP port for the voice server, dispatch panel, and admin panel. Must be free and open
authToken"CHANGE_ME"Shared secret between the game server and the voice server. Use 20+ random characters
adminPassword"141"Web admin panel password. "" disables password login (Discord auth then required)
dispatchNacId"141"Passphrase dispatchers type on the dispatch panel login screen. Not related to in-game channel access

dispatchNacId is not an access control

dispatchNacId, a codeplug's nac, and the NAC shown on a radio screen are three different things. None of them gate which channels a player can use — that is entirely codeplugs and ACE grants.

Discord authentication

Optional OAuth for the dispatch panel and the admin panel. Create an application at the Discord Developer Portal, then add both redirect URIs under OAuth2 → Redirects:

http://203.0.113.10:7777/radio/dispatch/auth
http://203.0.113.10:7777/admin/auth/callback
Config = {
    discord = {
        authEnabled  = true,
        clientId     = "1284739201847362048",
        clientSecret = "<your discord client secret>",
        guildId      = "938271046582910384",
        roles        = "938271046582910111,938271046582910222",
        adminRoles   = "938271046582910333",
        redirectUri  = "",
    },
}
KeyDefaultDescription
authEnabledfalseRequire Discord login for the dispatch panel. Every other field is ignored when this is false
clientId""Discord application client ID
clientSecret""Discord application client secret
guildId""Your Discord server (guild) ID
roles""Comma-separated role IDs allowed into the dispatch panel. "" allows any guild member
adminRoles""Comma-separated role IDs allowed into the admin panel. "" disables Discord admin access
redirectUri""Override the OAuth redirect. Only needed behind a reverse proxy or custom domain

Password and Discord login can both be active — the login page shows both. Dispatch sessions last 24 hours; role changes take effect at next login.

Framework detection

Config = {
    framework          = 'auto',
    frameworkResources = {
        esx = 'es_extended', qbcore = 'qb-core', qbx = 'qbx_core',
        nd = 'ND_Core', ox_core = 'ox_core', nat2k15 = 'framework',
    },
    autoRefreshOnJobChange = true,
}
KeyDefaultDescription
framework'auto''auto', 'esx', 'qbcore', 'qbx', 'nd', 'ox_core', 'nat2k15', or 'custom'
frameworkResourcessee aboveResource names used for detection. Only change these if you renamed a framework resource
autoRefreshOnJobChangetrueListen for your framework's job-change event and refresh the player's codeplug immediately instead of waiting for the ~15 s poll

'auto' probes in order: qbx → qbcore → esx → nd → ox_core → nat2k15, falling back to 'custom'. The job-change listeners live in escrow-protected server code, not in sv_functions.lua, so they survive an upgrade even if you keep a customised sv_functions.lua. Set autoRefreshOnJobChange = false only if your own code already calls exports['tRadio']:refreshNacId(source).

Inventory item requirement

Config = {
    inventory = {
        enabled = true,
        item    = 'radio',
        system  = 'auto',
    },
}
KeyDefaultDescription
enabledfalseWhen true, a player must carry the item to open the radio
item'radio'Item name as registered in your inventory resource
system'auto''auto', 'ox_inventory', 'qb', 'esx', or 'custom'

'auto' tries ox_inventory first, then falls back to the active framework's inventory. When the item requirement is enabled, tRadio also registers the item as usable automatically for ox_inventory, QBCore, QBX, and ESX — using it from the inventory opens the radio. You do not need to write a CreateUseableItem call.

Register the item in your inventory first

tRadio never creates the item. Add it to your inventory resource's item list before enabling this. On ox_inventory installs bridged to ESX or QBox, item use is routed before tRadio's hook is consulted — if using the item does nothing, add client.event = "radio:client:openRadio" to the radio entry in your items.lua.

Grant the tRadio.access ACE to let specific players or groups bypass the item check. For an unsupported inventory such as VORP, set system = 'custom' and replace Config.playerHasRadioItem in sv_functions.lua:

Config.playerHasRadioItem = function(serverId)
    local item = (Config.inventory and Config.inventory.item) or 'radio'
    local p = promise.new()
    exports.vorp_inventory:getItemCount(serverId, item, function(count) p:resolve(count) end)
    return (Citizen.Await(p) or 0) >= 1
end

Framework integration

Assigning codeplugs to jobs

A player needs a codeplug to open a radio at all. Map jobs to codeplug IDs in config/sv_functions.lua. Keys must match a codeplug's id in the admin panel. Changes require restart tRadio.

config/sv_functions.lua (shipped)

local JOB_CODEPLUG_MAP = {
    ['police']    = 'leo',
    ['sheriff']   = 'leo',
    ['ambulance'] = 'leo',
    ['fire']      = 'leo',
}

The lookup is case-insensitive, and works unchanged on ESX, QBCore, QBX, ND_Core, ox_core, and nat2k15 — the framework-specific job lookup is already written for you. On nat2k15, keys are dept level keys ('lspd_level', 'bcso_level') rather than job names.

Different codeplugs per rank. A value can be a table of grade thresholds instead of a plain string. The highest threshold at or below the player's grade wins:

local JOB_CODEPLUG_MAP = {
    ['police'] = { [0] = 'leo', [3] = 'leo-supervisor', [8] = 'leo-command' },
    ['fire']   = 'fire',
}

Grades 0–2 get leo, 3–7 get leo-supervisor, 8+ get leo-command. A grade below the lowest key gets no codeplug — the same secure default as an unmapped job.

Unmapped jobs get no radio, on purpose

Civilians and any job not listed receive no codeplug. Returning a fallback codeplug would silently grant that job whatever the fallback allows — and a codeplug with empty access lists resolves to "every zone". If you do want open access, uncomment the return 'leo' line at the bottom of Config.getPlayerCodeplugId and make sure that codeplug's lists are deliberately broad.

With framework = 'custom' and no changes, every player receives the leo codeplug.

Server hooks

All four live in config/sv_functions.lua and ship with working implementations for every supported framework. Edit them only to change behaviour.

HookReturnsCalled
Config.getPlayerAccess(serverId)booleanOn every server-side radio event. false denies the radio entirely. The shipped version checks the inventory item (when enabled) and then requires a codeplug
Config.playerHasRadioItem(serverId)booleanOnly when Config.inventory.enabled is true. Replace this when system = 'custom'
Config.getPlayerCodeplugId(serverId)codeplug ID string or nilOn access resolution. The shipped version reads JOB_CODEPLUG_MAP
Config.getPlayerName(serverId)stringFor the name shown on radios and the dispatch panel. Falls back to extracting a callsign suffix such as 1A-12 from the server display name

Refreshing access after a job change

With autoRefreshOnJobChange = true (the default) this is automatic on QBCore, QBX, ESX, ND_Core, and ox_core. nat2k15 has no job-change event and relies on the ~15 s poll.

To trigger it yourself from your own resource:

exports['tRadio']:refreshNacId(source)

Access is re-resolved immediately and pushed to the client. A player sitting on a channel they can no longer reach is disconnected from it.

Client hooks

In config/cl_functions.lua.

Config.canTalk() — runs while PTT is held. Return false to block transmission.

Config.canTalk = function()
    if IsPlayerDead(PlayerId()) then return false end
    if IsPedSwimming(PlayerPedId()) then return false end
    return true
end

If this starts returning false mid-transmission, the client force-stops PTT and logs canTalk check failed mid-transmission to F8. This is the most common non-hardware cause of "my mic doesn't work".

Config.batteryTick(currentBattery, deltaTime) — returns the new battery level. deltaTime is in seconds, so rates are battery-percent per second.

Config.batteryTick = function(currentBattery, deltaTime)
    local vehicle = GetVehiclePedIsIn(PlayerPedId(), false)
    if vehicle ~= 0 then
        return math.min(100.0, currentBattery + (0.5 * deltaTime)) -- full charge in ~3 min
    else
        return math.max(0.0, currentBattery - (0.1 * deltaTime))   -- full drain in ~17 min
    end
end

Charging in any vehicle, driver or passenger, with no dock prop, is intentional. Add your own vehicle-type or prop condition here to restrict it. To switch battery drain off entirely, use Battery Drain in Admin Panel → Settings → General — that skips this hook and holds every battery at 100%.

Config.bgModeCheck() — polled about every 500 ms. Returns a mode string that selects the background sound other players hear behind your voice, or false for silence. The shipped version returns "helicopter" when piloting a helicopter, "k9" when a dog ped from Config.k9Models is in a passenger seat, and otherwise delegates to the configured siren provider. Mode strings map to WAV files in Admin Panel → Sounds → Background Sound Modes; see Background sounds.

Config.k9Models — ped model names that trigger "k9". Ships with seven GTA dog models; add your own custom K9 peds here.

Config.animations — the animation list players choose from in their settings. See Animations.


Zones and channels

Managed live in Admin Panel → Zones & Channels. Changes reach every connected client in real time.

A zone is a group of channels — usually a region or an agency. A channel is one frequency inside it.

Channel types

Conventional — one shared frequency. Everyone connected hears everyone else regardless of location. Use this for almost everything.

Trunked — units are assigned a sub-frequency automatically based on where they are, so crews in different parts of the map do not step on each other. Dispatchers reach every unit through the control frequency. Requires a frequency range (the sub-frequency pool) and a coverage radius in metres (the cell size).

Channel fields

FieldTypeDefaultDescription
NamestringDisplay name on the radio, e.g. DISP
FrequencynumberMHz. Must be unique across all zones
Typeselectconventionalconventional or trunked
Modeselectanalogp25_digital runs this channel's audio through the P25 IMBE vocoder; analog does not
DispatchtoggleoffMonitor only — dispatchers can listen but not transmit (stores canTransmit: false)
Frequency rangetwo numbersTrunked only. Start and end of the sub-frequency pool
CoveragenumberTrunked only. Cell radius in metres
Encryptionselectclearsecure marks the channel encrypted on the display
GPS colour overrideblip IDunsetOverrides the codeplug's GPS colour for players on this channel only

Each channel has a generated channel ID shown in small text under its name (for example disp-lx1gi). That ID is what codeplugs reference — not the frequency, and not the name.

Hiding a zone from dispatch

Each zone has a Hide from dispatch toggle. A hidden zone is withheld from the dispatch panel entirely: its channels do not appear, and its traffic stays out of the transmission log and user counts. In-game it works exactly as before.

Use it for zones that exist purely for roleplay — civilian, criminal, or business radio — that would otherwise clutter a dispatcher's board.

This is display scope, not access control

Hiding a zone does not change which players can use its channels. To restrict player access, use codeplug channel access. A dispatcher connected to or scanning a channel when you hide its zone is removed from it on the next config refresh, and announcement groups skip hidden channels when broadcast.

Announcement groups

Also on the Zones & Channels page. A group is a named set of channel IDs. A dispatcher arms a group and their next PTT fans out to every channel in it simultaneously. The panel shows a GROUPS bar above the zone list whenever at least one group exists, and each group can get its own PTT key.

Two ship by default: * DISP and * C2C, each spanning all three starter zones.

Geo zones

Admin Panel → Geo Zones. A circular area that switches a player's radio to a target frequency when they walk in, and switches back when they leave.

FieldDescription
Position (X, Y, Z)World coordinates of the centre
RadiusMetres
Vertical boundsOptional min/max world Z, for stacked areas like parking garages. Min must be ≤ max
Target frequencyMust match a frequency on a configured channel, or the save is rejected

Auto-switching is opt-in per player and per zone. A player must enable both the master Geo Auto-Switch toggle and the individual zone's checkbox in /tradio → Style. Zones a player's codeplug cannot reach are hidden from that list.

Use /_tradio_coords in-game to copy your current position straight into the X/Y/Z fields.


Codeplugs

A codeplug is a radio personality: which channels a player gets, which radio models they can pick, which features are on, and what their emergency button does. Managed in Admin Panel → Codeplugs; job assignment happens in sv_functions.lua.

Codeplug sections

SectionWhat you set
IdentityID (locked after creation — it is what JOB_CODEPLUG_MAP and ACE grants reference), name, and NAC. The NAC is cosmetic: it shows on the radio screen and dispatch panel and gates nothing
Allowed ModelsWhich radio models players may select. Selecting none leaves them with no radio
Default LayoutsThe model shown first per context — on-foot focused, on-foot unfocused, ground vehicle, watercraft, air vehicle. Also per-spawncode and per-spawncode+livery overrides, which take priority
Channel AccessChannels this codeplug can connect and transmit on. Scan access is granted automatically for these. Zone access is derived — a zone is granted as soon as any of its channels is selected
Scan AccessExtra listen-only channels on top of the connect-access ones
Scan ListsNamed presets players load from /tradio → Scan Lists to populate their scan channels in one click. Each entry has a priority: 1 High, 2 Normal, 3 Low
FeaturesGPS Blips, Scan, Emergency Button, Man Down
Supervisor AccessGrants the in-game SGN sign-on button and trunked control-channel access, without granting server admin rights
Emergency ConfigurationWhich channel to switch to on emergency (or stay put), auto-transmit on/off, transmit duration in seconds, and panic tone repeat interval
GPS VisibilityWhich other codeplugs' blips a player on this codeplug can see, plus this codeplug's own blip colour and its flash colour during panic

Minimum viable codeplug: an ID, a name, at least one allowed model, and at least one channel. Everything else has a safe default.

Codeplug changes reach a player the next time their job loads or you call refreshNacId.

How access is resolved

Every radio open attempt and every refreshNacId call runs this chain. A player gets the union of codeplug access and ACE grants.

StepGateDenied when
1Config.getPlayerAccess(serverId)Returns false — no radio at all
2Codeplug assignment via JOB_CODEPLUG_MAP, Config.getPlayerCodeplugId, or tRadio.codeplug.{id} ACENo codeplug — no radio
3Zone visibility — derived from selected channels, or tRadio.zone.{N} ACENo channel in the zone and no ACE
4Channel connect — codeplug channel access or tRadio.connect.{freq} ACENeither — channel hidden
5Scan — codeplug scan access, connect access, or tRadio.scan.{freq} ACENeither — no scan
6GPS visibility — codeplug GPS Visibility list or tRadio.gps.{freq} ACENeither — no blips
7Supervisor controls (SGN, trunked CT/TK)Requires tlib.admin ACE or the codeplug's Supervisor toggle

ACE permissions

An alternative or supplement to codeplug assignment. tRadio.codeplug.{id} has the highest priority and overrides JOB_CODEPLUG_MAP.

ACEGrants
tRadio.accessBypass the inventory item requirement
tRadio.codeplug.{id}Assign codeplug {id} directly
tRadio.connect.{freq}Connect and transmit on a frequency
tRadio.scan.{freq}Scan (listen only) a frequency
tRadio.gps.{freq}See GPS blips on a frequency
tRadio.zone.{N}Access zone N by its 1-based position in the zone list
tlib.adminIn-game SGN button, trunked CT/TK controls, and the 3D prop editor. Does not unlock the web admin panel

Frequency keys drop the decimal point: 154.815154815, 856.11258561125.

server.cfg

add_ace group.admin tRadio.access allow
add_ace identifier.steam:110000100000001 tRadio.codeplug.leo allow
add_ace group.ems tRadio.connect.154755 allow
add_ace group.ems tRadio.scan.8561125 allow
add_ace group.ems tRadio.gps.154755 allow
add_ace group.ems tRadio.zone.1 allow
add_ace group.admin tlib.admin allow

tRadio.zone.N is positional

Zone ACEs key off a zone's position in the list, not a stable ID. Deleting a zone shifts every later-numbered grant. Review your ACE config after reordering or deleting zones — the admin panel warns you at the point of deletion.


Server settings

Admin Panel → Server Settings, four tabs. Every value below applies live to all connected clients unless marked restart required.

General tab

SettingDefaultDescription
Check for UpdatestrueCheck for new tRadio versions on start. Read live
Include Pre-release UpdatesfalseAlso pull release candidates. For testers. Read live
911 CallingtrueExperimental. Master switch for the whole 911 system. Disabling is enforced immediately server-side, but the /911 command itself only disappears for players who reconnect. Restart required
911 Call Cooldown30 sExperimental. Minimum wait between a player's 911 calls. 0 disables it; a 5-second server backstop still applies. Read live
Low Battery ChirptrueWarning chirp under 50% battery, more frequent under 30%
Battery DraintrueWhen off, every battery is held at 100% and Config.batteryTick is skipped
Log Level30 Error · 1 Warnings · 2 Minimal · 3 Normal · 4 Debug · 5 Verbose. Applies live
Radio BlipstrueMaster switch for minimap blips of players on your channel
Blip Update Rate50 msClient-side render interval. Below 50 ms is not recommended
Radio AnimationstrueShow the animation selector in player settings
Focus Layout ModefalseDefault for new players — a separate radio skin when the UI is open but unfocused. Needs a HandheldUnfocused default layout
Enable CallsignstrueLet players set a personal callsign
Callsign CommandcallsignThe chat command name, without the slash. Restart required
Man-Down by DefaultfalseNew players have man-down monitoring on
Warning Delay45 sHow long a player must be down before the warning tone. Range 10–300
Emergency Delay20 sTime after the warning before it escalates to a full emergency broadcast. Range 5–120
TTS by DefaulttrueNew players hear spoken zone and channel announcements when switching

Audio & Radio tab

Volume scales differ

The admin panel and dispatch panel use 0–100. The Lua export API uses 0.0–1.0. Divide by 100 when passing a config value to an export: voiceVolume = 65setVolume(0.65).

Audio Defaults — applied when a player first connects; they can override them.

SettingDefaultDescription
Voice Volume65Default radio voice volume (0–100)
SFX Volume35Default tone and effect volume (0–100)
3D Audio Volume50Default 3D spatial volume (0–100)
Volume Step5Change per press of a volume keybind. Range 1–25

PTT & Transmission

SettingDefaultDescription
PTT Release Delay350 msHow long the radio stays open after releasing PTT, so speech is not clipped. Range 0–2000
PTT Triggers ProximitytrueWith no radio equipped, PTT activates proximity voice instead of doing nothing. Requires MumbleVoice proximity audio
Transmission TonestruePlay TX_START / TX_END tones when someone begins or ends transmitting
Analog TonestrueUse analog-style squelch sounds (PTT / PTT_END, heard by the transmitter) instead of the digital receiver tones
Panic Alert Timeout60 sHow long a panic stays active before auto-clearing. 0 = manual clearance only
Panic Alert Cooldown15 sMinimum wait before the same player can panic again on that channel

Half-Duplex (Bonking) — what happens when someone keys up while another unit is talking.

SettingDefaultDescription
Block TransmissiontrueDeny the PTT attempt. When off, the transmission goes through but a collision tone plays
Play Bonk TonetruePlay a tone to the blocked user
Bonk Tone to AlltrueEveryone on the channel hears it, not just the sender
Double-Tap OverridetrueTwo quick PTT presses force a blocked transmission through
Double-Tap Window1500 msHow close the two presses must be. Range 100–5000

3D Positional Audio — everything below the master switch is hidden until it is on.

SettingDefaultDescription
Enable 3D AudiofalseMaster switch — allows players to turn 3D audio on
3D Audio On by DefaultfalseNew players start with it enabled (their Earbuds toggle off)
Vehicle 3D AudiotrueA vehicle the player has been in keeps rebroadcasting their radio from itself
Vehicle 3D Distance3 mHow far the player must walk from the vehicle before it takes over as the source
Player Speaker Range35 mHow far a handheld radio carries
Vehicle Speaker Range60 mHow far a vehicle-sourced radio carries

Signal Coverage

SettingDefaultDescription
Always Full SignalfalseForce full bars and no degradation, without deleting your tower configuration
Signal DegradationfalseReduce audio quality with distance from the nearest tower
Falloff Rate24How fast quality drops with distance. 0 gradual, 100 steep
Degradation Intensity50How severe the degradation is at low signal

Degradation does nothing until towers exist — see Signal towers.

Dispatch Scanning

SettingDefaultDescription
Simultaneous Scan AudiofalseOff: a transmission on the dispatcher's main channel mutes scanned channels until it ends. On: every scanned channel plays independently, overlapping

Radio Audio Effects — see Radio FX.

Controls tab

Default keybinds registered through FiveM's RegisterKeyMapping. Players can rebind any of them in FiveM → Settings → Key Bindings → FiveM. Click a field to capture a key, right-click to clear it (registering the action unbound).

ActionDefault
Push-to-TalkB
Toggle RadioF6
Voice Volume Up / Down= / -
SFX Volume Up / Down] / [
Close Radio, Power, Channel Up/Down, Zone Up/Down, Menu Up/Down/Left/Right/Home, Menu Buttons 1–3, Emergencyunbound
Style Up / Downunbound — hidden unless Radio Animations is on
3D Volume Up / Downunbound — hidden unless 3D Audio is on

Advanced tab

Earpiece / Items — GTA outfit slots that switch a player's earbuds on automatically. With earbuds active a player emits no 3D audio, so nearby players cannot hear their radio.

FieldDescription
Typeprop for accessories, component for clothing
SlotThe GTA slot — props include Hat/Helmet (0), Glasses (1), Ear/Earpiece (2), Watch (6), Bracelet (7)
Drawable IDThe 0-based clothing variant index. Find it with a ped editor

Clothing is checked every 7 seconds. A manual earbud toggle is respected until the player's clothing changes again.

LB Phone / LB Radio Integration — see LB Phone and LB Radio. Experimental.

ImperialCAD Integration — see ImperialCAD.


Sounds

Admin Panel → Sounds. All changes broadcast instantly.

Tone catalog

Every tone is a named slot mapped to a WAV file. Map them under Global Tone Mappings, and upload new WAVs from Marketplace → Sounds.

TonePlays when
PTT / PTT_TRUNKED / PTT_PATCHEDYou key up — conventional / trunked / patched
PTT_END / PTT_END_TRUNKED / PTT_END_PATCHEDYou release
TX_START / TX_START_TRUNKED / TX_START_PATCHEDSomeone else keys up
TX_END / TX_END_TRUNKED / TX_END_PATCHEDSomeone else releases
BEEPGeneric alert beep
BONKTransmission collision
CHIRPShort acknowledgement, also the low-battery chirp
ECHOMessage received
PANICPanic button activation
ALERT_A / ALERT_B / ALERT_CDefault alert-tone slots
ALERT_SIGNAL3Signal 3 alert tone
PRIORITY / PRIORITY_REPEATPriority alert activation and each repeat
RING_OUTHeard by a 911 caller while their call rings
RING_INHeard by dispatchers when a 911 call arrives

Per-model overrides replace a single tone for one radio model only, taking priority over the global mapping. You can also drop WAVs directly at layouts/{MODEL}/sounds/{TONE_NAME}.wav.

Background sounds

These fixed-filename WAVs in layouts/sounds/ play during a received transmission and cannot be renamed:

FilePurpose
transStart.wavClick-in at the start of a received transmission
transMid.wavLooping background noise during it
transEnd.wav / transEnd1.wav / transEnd2.wavClick-out — one of the three is picked at random
bgSiren.wavSiren loop
bgHeli.wavHelicopter rotors
bgDog.wavK9
bgShot.wavGunshot fallback

Background Sound Modes maps the strings Config.bgModeCheck returns to WAV files. Three ship mapped: sirenbgSiren.wav, helicopterbgHeli.wav, k9bgDog.wav. Return any other string from bgModeCheck, add the mapping here, and upload the WAV — unmapped modes simply play nothing.

For example, to play a different WAV per LVC siren tone, return distinct modes from bgModeCheck:

if tone == 2 then return "sirenWail" end
if tone == 3 then return "sirenYelp" end
if tone > 0  then return "siren"     end

then map sirenWail and sirenYelp here.

Siren detection

Which ELS resource bgModeCheck reads to decide a siren is on.

ProviderValue
Auto — try each installed provider in orderauto (default)
tELS onlytels
Luxart Vehicle Control onlylvc
LVC Fleet onlylvc_fleet
ELS-FiveM onlyels_fivem
JLS onlyjls
Advanced Lighting System onlyals
Native GTA siren onlynative
Skip detection — implement bgModeCheck yourselfcustom

Auto order is tELS → LVC / LVC Fleet → ELS-FiveM → JLS → ALS → native. Each provider returns "not my vehicle" when it does not recognise the car, so mixed fleets work. Resource-name overrides for tELS, LVC, and JLS are on the same page and only needed if you renamed one; LVC Fleet, ALS, and ELS-FiveM use events and need no name.

To force an immediate re-poll after a custom siren state change:

exports['tRadio']:NotifySirenChanged()

Radio FX

The DSP chain applied to all incoming radio voice: mic → input gain → highpass → lowpass → compressor → limiter → saturation → mid EQ → output.

SettingDefaultRange
Radio FXtrueMaster switch. Everything below is hidden when off
Distortion Modeclassicclassic hard-clip, tube smooth saturation
Highpass Cutoff250 Hz80–800
Lowpass Cutoff3500 Hz1200–8000
Compression600–100
Distortion300–100
Mid Boost2 dB−12 to 12
Input Gain1.650.5–3.0
ProblemFix
Too muffled, telephone-likeRaise Lowpass Cutoff to 4000–5000
Too tinny or harshLower Highpass Cutoff to 150–200
Volume pumpingLower Compression
Crackling or clippedLower Input Gain to 0.8–1.2, or lower Distortion

P25 is separate and per-channel. There is no global P25 switch. Set a channel's Mode to P25 Digital and every listener on it — in-game, dispatch, and 3D bystanders — hears real IMBE vocoder audio. The analog FX chain applies on top. The first transmission of a session has a brief WASM warm-up.


Alerts

Admin Panel → Alerts. An alert puts a coloured banner on every radio connected to the channel. The first alert in the list is what the in-game SGN button fires.

An alert has three phases, each with an optional tone: activate (once), repeat (persistent alerts only, every repeatInterval), and deactivate (once, on clear).

FieldDefaultDescription
NameDisplay text on the radio
ColorHex colour of the banner
PersistentfalseStays active until manually cleared
ToneSingle tone used for all phases
Multi-toneoffSwitches to separate activate / repeat / deactivate tones
Repeat everyrandom 5–10 sGap between repeats. Minimum 1000 ms
Show banner on repeattrueFlash the banner on each repeat
Deactivate labelRESUMEBanner text shown on clearance
Deactivate color#126300Colour of the clearance banner
Tone OnlyfalsePlay the tone with no visual banner
Channel filterall channelsRestrict this alert to specific channels

Four ship by default: SIGNAL 100, SIGNAL 3, Ping, and Bonk.


Speakers

Admin Panel → Speakers. World-placed tone-board speakers that dispatchers can broadcast a tone to from the panel — station alerting, tone-outs, PA stingers.

FieldDefaultDescription
Position (X, Y, Z)World coordinates
Heading0Prop rotation in degrees. Only affects the visible prop
Radius60 mAudible reach. Controls both who receives the tone and how far it carries. Applies from the next tone
Prop ModelnoneOptional. Leave blank to place no prop — the speaker still plays from its coordinates
GroupnoneSpeakers sharing a group name are one selectable target on the dispatch panel

Grab coordinates in-game with /_tradio_coords. Dispatchers use them from the Speakers button in the dispatch panel; see Usage.


Signal towers

Admin Panel → Towers. Physical towers that provide coverage. Signal strength falls off with distance from the nearest one, and with Signal Degradation on, audio quality degrades with it.

FieldDescription
NameLabel in the panel
Position (X, Y, Z)World coordinates
Prop modelOptional. Blank places no prop — the tower still provides coverage

Signal strength is measured from the nearest tower, so coverage is a union of each tower's reach rather than a sum. Place them where you want usable comms, and leave gaps where you want players to lose signal.

Towers configured before this page existed are migrated automatically on first start, with no action needed.


Integrations

LB Phone and LB Radio

Fully managed in Admin Panel → Settings → Advanced → LB Phone / LB Radio Integration. Nothing to configure in config.lua.

Experimental

Both integrations are experimental, and the phone dial-in is an entry point into the 911 system, which is experimental in its own right. Leave them off if you would rather not run them on a live server yet.

SettingDefaultDescription
LB Phone 911 Dial-InOffPlayers dial 911 from the in-game phone and receive call status as phone notifications. Requires lb-phone
LB Radio Frequency MirrorOffMirrors the player's frequency, connection status, and user count into LB Phone's Radio app. Read-only and cosmetic. Requires lb-radio

The two are independent — you can run either one alone, or neither.

The phone integration replaces /911 — it does not add to it

While LB Phone 911 Dial-In is on, the /911 chat command is not registered at all. Both routes reach the same queue, so nothing else changes. If lb-phone is not actually installed and started, players have no way to reach 911 — tRadio logs a warning at startup when it detects exactly this. Turn the setting off to get the chat command back.

The phone dial-in carries a restart badge: each player is given exactly one 911 entry point when they connect, and FiveM cannot un-register a chat command or an lb-phone custom number mid-session. Already-connected players keep whichever entry point they were given until they reconnect or the resource restarts. The mirror has no such constraint and applies live.

The 911 system as a whole is switched on and off in Admin Panel → Settings → General → 911 Calling.

ImperialCAD

Fully managed in Admin Panel → Settings → Advanced. Nothing to configure in config.lua, and both toggles apply live.

SettingDefaultDescription
Enable ImperialCAD IntegrationfalseMirror every queued 911 call into ImperialCAD as a parallel call record
Sync Panic to ImperialCADtrueAlso mirror radio panic activations into ImperialCAD's Panic() / ClearPanic()

Requires the ImperialCAD resource to be started under exactly that name, with its own imperial_community_id and imperialAPI convars set per ImperialCAD's own docs. tRadio only calls its exports. If the integration is on but the resource is not running, tRadio logs the reason at startup and on every call rather than failing silently.

Call ownership. A mirrored call is deleted from ImperialCAD only if it dies unanswered — queue timeout, or the caller hangs up while still ringing. Once a tRadio dispatcher accepts it, tRadio never touches ImperialCAD's copy again, on the assumption a CAD dispatcher now owns it.

911 hooks

Two optional hooks in config/sv_functions.lua. Both have sensible defaults and neither needs editing.

-- The caller ID a dispatcher sees. A real 911 dispatcher only ever gets a number,
-- so this — not getPlayerName — is what the call card shows.
-- Default: the caller's real LB Phone number when available, otherwise a stable
-- 555-XXXX placeholder derived from their license, so the same player always gets
-- the same number across sessions.
Config.getCall911CallerNumber = function(serverId)
    return "555-0142"
end

-- Fires when a call is placed while no dispatch panel is connected. The call still
-- queues and appears the moment a dispatcher logs in — this is an immediate,
-- best-effort notice so it is not silently missed.
-- Default: chat-broadcast to every player with radio access.
-- `call` fields: callerName, callerUnitId, callerNumber, message, coords, frequency, createdAt.
Config.call911NoDispatcherFallback = function(call)
    -- Discord webhook, NPC dispatch, or your own alerting here
end

Hook in from your own resource instead

radio:911:noDispatcherOnline(call) fires as a normal server event, so you can AddEventHandler it from your own resource rather than editing sv_functions.lua — which keeps your customisations across a tRadio update. radio:911:answered and radio:911:ended work the same way.


Multi-server federation

Run several game servers against one shared voice server and dispatch panel, so dispatchers see every unit in one place.

  • Host (Server A) runs the voice backend. Leave isSatelliteServer = false and keep its own serverAddress and authToken.
  • Satellites (Server B and up) run no backend of their own. They point at the host and federate player, panic, tone, and alert state to it over HTTP.

config/config.lua on each satellite:

Config = {
    serverAddress     = "http://203.0.113.10:7777", -- Server A's public URL, port included
    isSatelliteServer = true,
    authToken         = "<same value as Server A>", -- must match Server A exactly
}

The authToken must match exactly

Federation requests are HMAC-signed with authToken. A mismatch fails validation and the satellite's players never appear on the host's dispatch panel.

A satellite binds no HTTP or Socket.IO server, so serverAddress must be set — an empty value is rejected with an explicit error at startup. Satellites also pull zone, channel, and codeplug config from the host roughly every 30 seconds, so an admin panel change on the host reaches them without a restart.

911 calls require the host's voice server, so a satellite's callers connect through Server A like everyone else.