Configuration
Complete configuration reference for Tommy's Radio — config.lua, framework hooks, zones and channels, codeplugs, every admin panel setting, sounds, and integrations.
Every setting tRadio has, and which of the five places it lives in. Where it lives decides whether it needs a restart.
Where each setting lives
| Source | Contains | Edited in | Applies |
|---|---|---|---|
config/config.lua | Network, auth, Discord, framework, inventory | Text editor | After restart tRadio |
config/sv_functions.lua | Server hooks — access, player names, 911 caller ID, AI tools | Text editor | After restart tRadio |
config/cl_functions.lua | Client hooks — canTalk, battery, background sound, animations | Text editor | After restart tRadio |
data.json | Zones, channels, codeplugs, alerts, speakers, towers, geo zones, all server settings | Admin panel | Instantly |
layouts/<Model>/ | How each radio model's screen is drawn — see Radio layouts | Text editor | After restart tRadio |
Check the admin panel first. Open it at /tradio → Admin Panel, or at <your tLib URL>/dispatch/admin.
Set these four, then stop
tlib_http_public_urlinserver.cfg— only if tLib's auto-detection gets your address wrong.authToken— 20+ random characters. Only federation reads it.adminPasswordanddispatchNacId, or Discord OAuth.141is the demo server's code.- Custom framework only: the hooks in
config/sv_functions.luaandconfig/cl_functions.lua.
config.lua
Server and authentication
Config = {
exposeWeb = true,
showDispatchInHub = true,
showAdminInHub = true,
useLegacySocketTransport = false,
hostServerUrl = "", -- satellites only
isSatelliteServer = false,
authToken = "", -- federation only
adminPassword = "", -- empty = generated on first boot
dispatchNacId = "141",
}| Key | Default | Description |
|---|---|---|
exposeWeb | true | Publish the browser panels through tLib. Not needed in game. Required on a federation host |
showDispatchInHub | true | List Dispatch on tLib's hub. Visibility only. Restart required |
showAdminInHub | true | List Radio Admin as a sibling destination. Hidden is not private. Restart required |
useLegacySocketTransport | false | Deprecated since 7.0. Game-client Socket.IO fallback. Needs exposeWeb = true. Leave it false |
hostServerUrl | "" | Satellite-only URL of the federation host, including its tLib mount path |
isSatelliteServer | false | Share a host's radio backend from another game server |
authToken | "" | Federation shared secret, 20+ random characters. Empty means the endpoint answers 401 |
adminPassword | "" | Web admin password. Empty generates one on first boot into data.json. A value set here wins |
dispatchNacId | "141" | Browser dispatch login passphrase, not channel access. Change it |
Public web addressing belongs to tLib: tlib_http_public_url and tlib_http_port. The URL is optional and takes a bare IP, an ip:port, or a domain.
Discord authentication
Optional OAuth for both panels. Create an application at the Discord Developer Portal, then add both redirect URIs under OAuth2 → Redirects:
http://203.0.113.10:7777/dispatch/radio/dispatch/auth
http://203.0.113.10:7777/dispatch/admin/auth/callback
The Panel row of the console banner is the exact base. Discord matches redirect URIs exactly, so a missing /dispatch mount segment fails with invalid_redirect_uri.
Config = {
discord = {
authEnabled = true,
clientId = "1284739201847362048",
clientSecret = "<your discord client secret>",
guildId = "938271046582910384",
roles = "938271046582910111,938271046582910222",
adminRoles = "938271046582910333",
redirectUri = "",
},
}| Key | Default | Description |
|---|---|---|
authEnabled | false | Require Discord login for the dispatch panel in a browser. In-game login is separate |
clientId | "" | Developer Portal → General Information |
clientSecret | "" | Developer Portal → OAuth2 |
guildId | "" | Easiest in Admin Panel → Credentials, which lists your servers |
roles | "" | Comma-separated role IDs. "" = any guild member |
adminRoles | "" | "" = no Discord admin access |
botToken | "" | Set from Credentials; never shown back to you |
redirectUri | "" | Override the OAuth origin. "" derives it from tLib's public URL. Only needed behind a reverse proxy |
Store a bot token in Credentials and the page turns every Discord role into a checkbox. Sessions last 24 hours; role changes apply at next login.
When you need a bot token. The browser login never does. The in-game login does; without one, in-game access rests on the tradio.dispatch / tradio.admin ACE permissions alone.
redirectUri must be an origin — https://radio.example.com, not radio.example.com. A path is ignored, and a bad value falls back to tLib's public URL.
Giving out radios by Discord role
Credentials → Role → codeplug. It stacks with the jobs / ace / item routes; a player admitted by both keeps the higher job grade.
Roles are read when a player first needs a codeplug, then every five minutes.
Most Discord-role resources already write ACE principals, so ace = 'tradio.leo' may suffice.
In-game panel access
Neither panel asks for a password or a NAC ID in game. The check runs before the panel opens: a player without access sees one chat line and no panel.
-
ACE permission —
tradio.dispatchandtradio.admin:cfgadd_ace group.admin tradio.admin allow add_ace group.mod tradio.dispatch allow -
Discord roles — only if a bot token is stored and
discord.guildIdis set. Your bot must be in the guild with the Server Members Intent enabled.
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,
}| Key | Default | Description |
|---|---|---|
framework | 'auto' | 'auto', 'esx', 'qbcore', 'qbx', 'nd', 'ox_core', 'nat2k15' or 'custom' |
frameworkResources | see above | Resource names used for detection. Change only if you renamed one |
autoRefreshOnJobChange | true | Listen for your framework's job-change event instead of the ~15 s poll |
'auto' probes qbx → qbcore → esx → nd → ox_core → nat2k15, falling back to 'custom'. nat2k15 always polls. 'custom' means you write the server hooks.
A framework you set yourself is never overridden.
Who gets a radio
A codeplug is the radio's programming, and holding one is access. No codeplug, no radio.
tRadio ships open. Out of the box every player is granted the leo codeplug, so the radio works the moment the resource starts, with or without a framework:
Config = {
codeplugGrants = {
leo = { everyone = true },
},
}Lock it down when your framework is up by replacing that line with the routes you want:
Config = {
codeplugGrants = {
leo = { jobs = { police = 0, sheriff = 0, ambulance = 0, fire = 0 } },
fire = { jobs = { fire = 0 } },
},
}Keys are codeplug IDs from Admin Panel → Codeplugs. Each entry lists ways to be granted it, and any one is enough:
| Kind | Example | Means |
|---|---|---|
jobs | { police = 0, sheriff = 2 } | Job name → minimum grade |
ace | 'tradio.leo' | An ACE grant |
item | 'leo_radio' | Carrying an item |
custom | function(src) return … end | Your own check, returning a boolean |
everyone | true | No restriction |
tradio.codeplug.<id> is always accepted as an ACE, listed here or not.
A player holds exactly one codeplug. When several match, the one demanding the highest job grade wins; a tie goes to codeplug ID, and the console names both.
Requiring a physical radio item
Config = {
requireItem = 'radio', -- nil requires no item
}An item required in addition to a codeplug, for everybody. It is checked first, so a player missing both is denied for the item. To exempt someone, grant their codeplug through ace, custom or everyone.
Inventory item requirement
Config = {
inventory = { system = 'auto' },
}| Key | Default | Description |
|---|---|---|
system | 'auto' | 'auto', 'ox_inventory', 'qb', 'esx' or 'custom' |
enabled | — | Retired — use Config.requireItem. Read only when requireItem is absent |
item | — | Retired — use Config.requireItem. Read only when requireItem is absent |
With Config.requireItem set, tRadio makes that item usable on ox_inventory, QBCore, QBX and ESX by itself. 'auto' detects what your inventory resource supports, including forks.
Carrying the item and using it are two things. A failed registration costs only opening-by-use, but a failed item check denies every player. See Every player is denied radio access.
Opening the radio by using the item
Route 1 — items.lua. Recommended, and universal. No exports, no framework, no minimum version.
-- ox_inventory/data/items.lua
['radio'] = {
label = 'Radio',
weight = 500,
client = { event = 'tradio:client:openRadio' },
},Route 2 — automatic registration. tRadio probes your inventory at startup and registers every method it can prove will fire:
| Method | Used when |
|---|---|
ox_inventory usingItem hook | ox_inventory reports 2.43.0 or newer |
ox_inventory:usedItem event | The hook could not be proven — forks, older ox, bridges |
QBCore / QBX CreateUseableItem | Those frameworks, including alongside ox_inventory |
ESX RegisterUsableItem | ESX, including alongside ox_inventory |
When nothing can be proven, tRadio prints the items.lua line to add plus a Detected: summary. Quote it in a ticket.
Custom and forked inventories
Two hooks in config/sv_functions.lua, both shipping with working defaults.
For the item check, set system = 'custom' and replace Config.playerHasRadioItem:
Config.playerHasRadioItem = function(serverId)
local item = Config.requireItem 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
endFor opening the radio by use, fill in Config.registerRadioItemUse, a stub returning false:
Config.registerRadioItemUse = function(itemName, openEvent)
exports['my_inventory']:RegisterUsableItem(itemName, function(source)
TriggerClientEvent(openEvent, source)
end)
return true
end- Called once at startup, in a
pcall, only whensystem = 'custom'or auto-detection proved nothing - Receives
itemNameandopenEvent; fire that event at the player - Return
trueif you registered something; anything else falls through to theitems.luaadvice
Scanner
A scanner is receive-only — no PTT, and the server refuses one. It is an ordinary codeplug with scanner = true, granted in codeplugGrants.
codeplugGrants = {
scanner = { item = 'police_scanner' },
}A Scanner codeplug ships as a default alongside LEO, carrying the three default zones and twelve channels with SWX-250 as its layout. It is granted to nobody until you map it.
A scanner with an empty channel list renders as a radio that does nothing — the usual reason a working scanner looks broken. Without a grant for a secure channel it shows ENC.
AI dispatcher
Config.aiDispatch is the only part of the dispatcher in a file. Whether it is on, its channels, callsign and voice are in Admin Panel → Settings → AI Dispatcher; what it may do is Config.aiDispatchTools.
Config.aiDispatch = {
providers = {}, -- all three local
localEngine = { root = "", textModel = "", transcribeModel = "", voicesDir = "" },
voices = { male = { "en_US-ryan-medium" }, female = { "en_US-amy-medium" } },
wakePhrases = { "dispatch" },
demeanor = "", -- measured
}| Key | Default | Description |
|---|---|---|
providers | {} | Per-capability hosted provider for text, speech and transcription. See Where inference runs |
localEngine | all "" | Override engine and model locations. Empty means the copies in this resource |
voices | Piper voices | Only the first of each is used. What a voice is depends on who serves speech |
wakePhrases | { "dispatch" } | Traffic not starting with one of these is unit-to-unit, and is left alone |
demeanor | "" | measured, brisk, veteran or formal. Blank is measured. See How it carries itself |
The rest is on AI dispatcher.
Vehicle radio prop
Where it sits is positioned in game per vehicle (/tradio → 3D Prop Editor in the sidebar, needs tlib.admin). This section is which model spawns.
Config = {
vehicleProp = {
default = 'ztr540',
models = {
{ model = 'ztr540', label = 'ZTR-540 (built in)' },
},
},
}| Key | Default | Description |
|---|---|---|
default | 'ztr540' | Model for a placement that names none. Anything not in models falls back to ztr540 |
models | see above | The list offered in the prop editor. model is the archetype name; label is cosmetic |
tRadio streams exactly one model itself, ztr540, which cannot be removed. The models list is read at startup.
model is the archetype name from the .ytyp, not the file name, and is case-insensitive. Get it wrong and the vehicle falls back to default.
Behind a reverse proxy
The panels are served on tLib's port, so that is what a proxy sits in front of. The service speaks plain HTTP, and a proxy on 127.0.0.1 needs no trusted-proxy entry.
- Forward WebSocket upgrades — HTTP-only forwarding serves the panel, then fails every connection
- Point the proxy at
tlib_http_port, never tRadio's internal loopback port - Raise the read and send timeouts — nginx cuts a quiet WebSocket at 60 seconds
location / {
proxy_pass http://127.0.0.1:7777; # must equal tlib_http_port
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Without these, nginx closes an idle WebSocket after 60s.
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}List your proxy. An unlisted one makes every player count as the same client for rate limiting.
set tradio_trusted_proxies "172.17.0.1, 10.0.0.5"Required whenever the proxy is not on localhost. Loopback is always trusted, an IPv4 entry also matches its IPv6-mapped form, and the value is read once at startup.
Framework integration
Assigning codeplugs to jobs
The mapping lives under codeplugGrants. sv_functions.lua now only answers what job does this player hold.
The lookup is case-insensitive on all six frameworks. On nat2k15, dept level keys ('lspd_level') and display names ('LSPD') both work. Every job the player holds counts, including ox_core groups and qb gangs.
Different codeplugs per rank. Give each its own minimum grade; the highest the player clears wins. Below, grades 0–2 get leo, 3–7 leo-supervisor and 8+ leo-command.
codeplugGrants = {
leo = { jobs = { police = 0 } },
['leo-supervisor'] = { jobs = { police = 3 } },
['leo-command'] = { jobs = { police = 8 } },
fire = { jobs = { fire = 0 } },
},With no framework running, jobs grants nobody. Use ace, item, custom or everyone on a standalone server.
Server hooks
All in config/sv_functions.lua, shipping with working implementations for every supported framework. Edit them only to change behavior.
| Hook | Returns | Called |
|---|---|---|
Config.getPlayerAccess(serverId) | boolean | Every server-side radio event. false denies the radio entirely |
Config.playerHasRadioItem(serverId, item) | boolean | Whenever an item is asked about. Replace when system = 'custom' |
Config.getPlayerCodeplugId(serverId) | codeplug ID or nil | Access resolution. Shipped version resolves Config.codeplugGrants |
Config.getPlayerName(serverId) | string | The name on radios and the dispatch panel |
getPlayerName's 'custom' branch falls back to GetPlayerName(serverId) — the Steam or FiveM display name, not an RP character name. Return a string, never nil; serverId ≤ 0 means the dispatcher.
Config.getPlayerName = function(serverId)
if not serverId or serverId <= 0 then return "DISPATCH" end
local p = MyFramework.GetPlayer(serverId)
if p and p.charinfo then
return p.charinfo.firstname .. ' ' .. p.charinfo.lastname
end
return "Player " .. serverId
endRefreshing access after a job change is automatic on QBCore, QBX, ESX, ND_Core and ox_core. To trigger it yourself, call exports['tRadio']:refreshNacId(source).
Client hooks
In config/cl_functions.lua.
Config.canTalk() — runs while PTT is held; return false to block transmission. Returning false mid-transmission force-stops PTT and logs canTalk check failed mid-transmission to F8 — the most common non-hardware cause of "my mic doesn't work".
Config.canTalk = function()
if IsPlayerDead(PlayerId()) then return false end
if IsPedSwimming(PlayerPedId()) then return false end
return true
endConfig.batteryTick(currentBattery, deltaTime) — returns the new battery level, deltaTime in seconds. As shipped, any vehicle charges. Battery Drain in Settings → General skips this hook entirely.
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
end
return math.max(0.0, currentBattery - (0.1 * deltaTime)) -- full drain in ~17 min
end| Hook | What it does |
|---|---|
Config.bgModeCheck() | Polled every 500 ms. Returns a mode string for the background sound behind your voice, or false. Ships with "helicopter", "k9" and the siren provider. See Background sounds |
Config.k9Models | Ped models that trigger "k9". Seven GTA dog models ship |
Config.animations | The animation list players choose from. See Animations |
Config.screenRange | Metres within which another vehicle's dash radio is drawn. Default 8.0; your own is exempt, 0 leaves only that |
Zones and channels
Managed live in Admin Panel → Zones & Channels. A zone is a group of channels; a channel is one frequency inside it.
A new zone is invisible until a codeplug references it
Players get channels from their codeplug, so a zone in no codeplug is invisible — no error, no warning. This is the most common "I set it up and nothing works" report. Two steps:
- Zones & Channels → Add Zone, then add channels and save
- Codeplugs → Channel Access, on every codeplug that should hear them
An unreferenced zone carries a Not in any codeplug badge, and saving a new one prompts once.
Channel types
Conventional — one shared frequency. Everyone connected hears everyone else regardless of location. Use this for almost everything.
Trunked — units get a sub-frequency by location, so crews across the map do not step on each other. Needs a frequency range and a coverage radius. Dispatchers work it two-way through its control frequency, and the log groups the talkgroup under one entry.
Direct / simplex channels
A conventional channel can use the Direct / Simplex RF path: radio-to-radio, no repeater, no towers. RF path is separate from Mode, so a direct channel can be analog or P25.
| Setting | Default | Range | What it does |
|---|---|---|---|
| Direct range | 1500 | 50–10000 m | Distance at which reception ends. Bars fall 5 to 1 across it |
| Obstructed range | 35 | 1–100 % | Share of range left when terrain sits between two nearby radios |
| Dispatch base radio | off | — | Fixed map position and routing bucket for dispatch |
- Reception is per radio. Two units 3 km apart can both talk without hearing each other. A two-bar lead wins; similar strength gets neither
- Channel busy is per radio too — only traffic your own radio hears blocks you
- Signal bars show what you are receiving, not coverage
- Speakers, vehicle radios, gunshots, explosions and sirens follow the same rule
- Players in different routing buckets never hear each other
- Dispatch needs a base radio, or the channel is left off the panel entirely
Obstruction checks are short-range — tested under 500 m, and can only shorten the range. Longer links use the distance envelope alone.
Direct channels cannot be trunked, patched or grouped — each would act as a hidden repeater. Channels sharing a frequency must share RF settings, or that frequency carries no direct traffic.
Channel fields
| Field | Type | Default | Description |
|---|---|---|---|
| Name | string | — | Display name on the radio, e.g. DISP |
| Frequency | number | — | MHz. Must be unique across all zones |
| Type | select | conventional | conventional or trunked |
| RF path | select | repeater | repeater or direct (stored as rfMode) |
| Mode | select | analog | p25_digital runs the channel through a vocoder |
| System type | select | imbe | Digital only (stored as codec): imbe is P25 Phase 1, c2-3200 is M17's Codec 2 |
| Dispatch | toggle | off | Monitor only — dispatchers listen, never transmit (canTransmit: false) |
| Frequency range | two numbers | — | Trunked only. Start and end of the sub-frequency pool |
| Coverage | number | — | Trunked only. Cell radius in metres |
| Security intent | select | clear | Marks the channel encrypted and lights the secure (⊘) icon. Does not scramble audio |
| GPS color override | blip ID | unset | Overrides the codeplug's GPS color for players on this channel |
What secure does. The icon shows steadily and blinks on an encrypted call, and an ungranted scanner shows ENC. It does not scramble audio.
Each channel has a generated channel ID shown under its name (disp-lx1gi). That ID is what codeplugs reference — not the frequency, not the name.
Trunked channels need a valid frequency range and coverage. Range start must be below range end and coverage above 0. Sub-frequencies step by 0.0125 MHz, so a narrower range hands out nothing.
A panel save is refused with the reason. A hand-edited file is coerced where it safely can be, so check the console after a hand-edit.
Frequencies, coverage and frequencyRange normalize to four decimal places. Startup also reports, by name, a missing Channels list, a missing or non-numeric frequency, a missing type, and two channels sharing a frequency inside one zone.
Reordering zones and channels
Each zone card and channel row has a drag handle (⠿), and the new order is what players see. Channels reorder only within their zone.
Hiding a zone from dispatch
A Hide from dispatch toggle keeps a zone's channels off the panel and its traffic out of the log and user counts. In game it works as before.
Announcement groups
A group is a named set of channel IDs: a dispatcher arms one and their next PTT fans out to all of them. Each can get its own PTT key. Two ship: * DISP and * C2C.
Geo zones
Admin Panel → Coverage → Areas. A circular area that switches a player's radio to a target frequency when they walk in, and back when they leave.
| Field | Description |
|---|---|
| Position (X, Y, Z) | World coordinates of the centre |
| Radius | Metres |
| Vertical bounds | Optional min/max world Z for stacked areas. Min must be ≤ max |
| Target frequency | Must match a configured channel, or the save is rejected |
Auto-switching is opt-in per player and per zone: the master Geo Auto-Switch toggle plus the zone's checkbox in /tradio → Behavior → Zones. /tradio_debug coords copies your position.
Codeplugs
A codeplug is a radio personality: which channels a player gets, which models they can pick, which features are on, and what the emergency button does. Managed in Admin Panel → Codeplugs; granted in codeplugGrants.
Codeplug sections
| Section | What you set |
|---|---|
| Identity | ID (locked after creation — grants reference it), name, and NAC. The NAC is cosmetic and gates nothing |
| Allowed Models | Which radio models players may select. Selecting none leaves them with no radio |
| Default Layouts | Model shown first per context: on-foot focused and unfocused, ground, water, air. Per-spawncode overrides win |
| Channel Access | Channels this codeplug can connect and transmit on. Scan access follows automatically. Zone access is derived |
| Scan Access | Extra listen-only channels |
| Scan Lists | Named lists, priorities, talkback and hang time, and which list each channel scans. See Scan lists |
| Home Softkeys | Softkey banks for conventional and trunked channels, plus the menu timeout. See Home softkey banks |
| Display | Clock format, date order, top display zone letter. See Display |
| Features | GPS Blips, Scan, Emergency Button, Man Down. GPS Blips off disallows GPS rather than defaulting it off — the server withholds the position |
| Supervisor Access | The in-game Alrt key and the trunked Ctrl/Trnk keys, without server admin rights |
| Emergency Configuration | Button hold, keep-alive, profiles, per-channel assignment, Man Down timers. See Emergency profiles |
| GPS Visibility | Which other codeplugs' blips this one sees, plus its own blip and panic flash colors |
| Scanner | scanner = true makes this codeplug receive-only. See Scanner |
Minimum viable codeplug: an ID, a name, one allowed model, one channel. Changes reach a player the next time their job loads or you call refreshNacId.
Scan lists
A radio scans the list of the channel it is on, the way a real one does. Players turn scanning on and off and edit lists on the radio; see Scanning.
| Priority | Meaning |
|---|---|
| Priority 1 | Takes over from any other scanned traffic; cannot be nuisance-deleted |
| Priority 2 | Takes over from non-priority channels; cannot be nuisance-deleted |
| Non-priority | Heard when nothing higher is busy. New entries start here |
A real radio has one Priority 1 and one Priority 2 per list, and the panel warns when a list has more. Giving a channel a priority takes it from the one that had it.
Which list each channel scans, under Scan Lists → Which list each channel scans:
| Setting | Choices |
|---|---|
| Default for this codeplug | Personal (built on the radio), No scan list, or one of the codeplug's lists |
| Per channel | Use default, No scan list, or a specific list |
With lists but no default, unassigned channels scan the first list. With none at all, every player gets a Personal list built with Edit Scan. Scan shows No scan list or List empty accordingly. Deleting a list returns its channels to the default.
Talkback and hang time. Each list has both, and a row above the lists sets them for the Personal list and anything on Use default:
| Setting | Choices | Meaning |
|---|---|---|
| Transmit while scanning | Use default, Selected channel, Talkback | Selected channel (the default): PTT uses the selected channel. Talkback: PTT answers on the scanned channel the radio is stopped or parked on |
| Hang time | 0.5–10 s (default 1) | How long the radio stays parked on a scanned channel after it goes quiet |
The panel warns when a Talkback list holds scan-only channels, where a stopped player gets the bonk. Satellite players cannot use talkback at all.
Home softkey banks
Codeplugs → Home Softkeys programs the Home softkey row. Two banks: conventional (and no channel) and trunked. Leave a bank on Use default, or Customize to choose and order the keys.
| Key | Action |
|---|---|
Zone / Chan | Select Zone / Select Chan |
Scan | Scan on/off; hold for Edit Scan |
ScnL | Edit Scan |
Nuis | Nuisance delete |
Loc | Location sharing |
Info | Info → Radio Info |
Optn | Options |
ZnUp / ZnDn | Change zone immediately |
ChUp / ChDn | Change channel immediately |
Vol | Opens Voice vol |
Clck | Read-only Clock screen |
Alrt | Supervisors: fires the first configured alert on the channel |
Ctrl / Trnk | Supervisors, trunked: control frequency / sub-frequency |
Defaults are ZnUp Zone ZnDn Chan Scan Nuis ScnL Optn Loc Info then Alrt (conventional) or Ctrl/Trnk (trunked). Clck, ChUp/ChDn and Vol appear only if added. That order leaves Chan on page two of a three-key radio.
Unusable keys are skipped and the row closes up: scan keys without scan, Loc without GPS, supervisor keys without Supervisor Access. The panel warns, without blocking, when a bank omits Zone, Chan or Optn.
Menu timeout. Off by default; Turn on sets 30 seconds, and 5–300 is allowed. An idle radio returns to Home with the MENU_INACTIVE_EXIT tone, discarding anything unconfirmed. See Menu timeout.
Display
| Setting | Choices |
|---|---|
| Clock | 12-hour (01:45pm, default) or 24-hour (13:45) |
| Date order | Month/Day/Year (default) or Day/Month/Year. A date shows only when Radio clock time is the player's computer |
| Top display zone letter | None (default), Basic bank (A–F for zones 1–6) or Enhanced bank (A–Y, one letter per three zones, zones 1–75) |
The time comes from Settings → General → Radio clock time. The zone letter appears only on the QRX-720H top view.
Emergency profiles
Codeplugs → Emergency Configuration programs the emergency button: every channel runs one profile. See Emergency for how each mode behaves.
| Setting | Meaning |
|---|---|
| Button hold | Hold time for the orange button, 50–6200 ms (default 1000). Ending is always a one-second hold |
| Keep radio on during emergency | The radio cannot be powered off until the emergency clears |
| Profiles | One or more named profiles; a codeplug always keeps at least one |
| Man Down timers | Pre-alert and post-alert seconds. Blank uses the server's Warning Delay / Emergency Delay |
Each profile has:
| Field | Choices |
|---|---|
| Mode | Alarm — ends on acknowledgment or exhausted retries. Call — a priority call, trunked only. Alarm + Call — alarm, then call on acknowledgment, exhausted retries, or PTT |
| Silent | Alarm and Alarm + Call only. The sending radio shows and plays nothing; dispatch still sees it |
| Channel | Tactical (stay put) or Revert to a channel this codeplug can use. A reverted radio returns when the emergency ends |
| Hot mic | 0–120 s of self-keyed microphone on the first emergency transmission. 0 = off. Auto Transmit refuses it |
| Alarm retry every / Retries | Repeat interval while unacknowledged, and retries before No acknowledge. Not used by Call |
Which profile each channel uses works like scan list assignment: a default plus per-channel overrides. A No emergency channel gives a short reject tone, a conventional channel inheriting Call runs it as Alarm + Call, and Emergency Button off disables emergency everywhere.
A codeplug saved before profiles existed shows as a single Default profile built from its original settings.
Server-wide safety limits sit outside the codeplug: Panic Alert Timeout and Panic Alert Cooldown in Settings → Audio & Radio.
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.
| Step | Gate | Denied when |
|---|---|---|
| 1 | Config.getPlayerAccess(serverId) | Returns false — no radio at all |
| 2 | Codeplug assignment via codeplugGrants, getPlayerCodeplugId, or tradio.codeplug.{id} | No codeplug — no radio |
| 3 | Zone visibility — derived from selected channels, or tradio.zone.{N} | No channel in the zone and no ACE |
| 4 | Channel connect — codeplug channel access or tradio.connect.{freq} | Neither — channel hidden |
| 5 | Scan — codeplug scan access, connect access, or tradio.scan.{freq} | Neither — no scan |
| 6 | GPS visibility — codeplug GPS Visibility list or tradio.gps.{freq} | Neither — no blips |
| 6a | On the map — the codeplug's GPS Blips feature, then the player's GPS switch | Either off — they see blips but are not one |
| 7 | Supervisor controls (Alrt, trunked Ctrl/Trnk) | Requires tlib.admin or the codeplug's Supervisor toggle |
Nothing configured? With no framework, no Discord mapping and only jobs routes, tRadio runs standalone: every job-mapped codeplug goes to every player, and the console prints one Standalone: line. Add any other route, and grants are read strictly.
ACE permissions
An alternative or supplement to codeplug assignment. tradio.codeplug.{id} has the highest priority and overrides codeplugGrants.
| ACE | Grants |
|---|---|
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.admin | In-game Alrt, trunked Ctrl/Trnk, and the 3D prop editor. Not the web admin panel |
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.zone.1 allow
add_ace group.admin tlib.admin allowFrequency keys drop the decimal point: 154.815 → 154815. A precise form keeps it as an underscore — 154_815 — because dropping it is lossy: 1.23 and 12.3 both become 123. tRadio warns on startup if yours collide.
Pre-7.0 tRadio.* spellings are still honoured, and the console names each once per start.
Server settings
Admin Panel → Server Settings, four tabs. Every value applies live to all connected clients unless marked restart required.
General tab
| Setting | Default | Description |
|---|---|---|
| Check for Updates | true | Check for new tRadio versions on start |
| Include Pre-release Updates | false | Also pull release candidates |
| 911 Calling | false | Master switch, off until you turn it on. Enforced immediately server-side, but /911 only disappears for players who reconnect. Restart required |
| 911 Call Cooldown | 30 s | Minimum wait between a player's calls. 0 disables it; a 5-second backstop still applies |
| Low Battery Chirp | true | Warning chirp under 50% battery, more frequent under 30% |
| Battery Drain | true | Off holds every battery at 100% and skips Config.batteryTick |
| Log Level | 3 | 0 Error · 1 Warnings · 2 Minimal · 3 Normal · 4 Debug · 5 Verbose |
| Radio Blips | true | Master switch for minimap blips of players on your channel |
| Blip Update Rate | 50 ms | Client-side render interval. Below 50 ms is not recommended |
| Radio Animations | true | Show the animation selector in player settings |
| Focus Layout Mode | false | A separate skin when the UI is open but unfocused. Needs a HandheldUnfocused layout |
| Enable Callsigns | true | Let players set a personal callsign |
| Callsign Command | callsign | Chat command name, without the slash. Restart required |
| Man-Down by Default | false | New players have man-down monitoring on |
| Warning Delay | 45 s | Motionless time before the Man Down post-alert. Range 10–300. A codeplug's timers override it |
| Emergency Delay | 20 s | Post-alert time before the emergency is raised. Range 5–120. A codeplug's timers override it |
| TTS by Default | true | New players hear spoken zone and channel announcements |
| Radio clock time | Game world | Game world: the in-game clock, no date. Player's computer: their local time and date. Format is per codeplug under Display |
The three Man-Down settings are the only ones that raise an emergency without a key press. Read Man down first.
Audio & Radio tab
Audio Defaults — applied when a player first connects.
| Setting | Default | Description |
|---|---|---|
| Voice Volume | 65 | Default radio voice volume (0–100) |
| Tone Volume | 35 | Default tone and effect volume (0–100) |
| 3D Audio Volume | 50 | Default 3D spatial volume (0–100) |
| Volume Step | 5 | Change per press of a volume keybind. Range 1–25 |
PTT & Transmission
| Setting | Default | Description |
|---|---|---|
| PTT Release Delay | 350 ms | How long the radio stays open after releasing PTT. Range 0–2000 |
| Transmit Time-Out | 60000 ms | The radio's own TOT: a key held this long is cut off, with a warning four seconds earlier. 0 turns it off; otherwise 5000–1800000, and less is raised to 5 s |
| PTT Triggers Proximity | true | With no radio equipped, PTT activates proximity voice. Requires MumbleVoice |
| Transmission Tones | true | Play TX_START / TX_END when someone begins or ends transmitting |
| Analog Tones | true | Use analog squelch sounds (PTT / PTT_END, heard by the transmitter) instead of digital receiver tones |
| Panic Alert Timeout | 60 s | How long an emergency stays active before auto-clearing, for crashed or AFK units. 0 = manual only |
| Panic Alert Cooldown | 15 s | Minimum wait before the same player raises another emergency there. Never blocks clearing one |
Half-Duplex (Bonking) — what happens when someone keys up over another unit.
| Setting | Default | Description |
|---|---|---|
| Block Transmission | true | Deny the PTT attempt. Off, it goes through but a collision tone plays |
| Play Bonk Tone | true | Play a tone to the blocked user |
| Bonk Tone to All | true | Everyone on the channel hears it |
| Double-Tap Override | true | Two quick PTT presses force a blocked transmission through |
| Double-Tap Window | 1500 ms | How close the presses must be. Range 100–5000 |
3D Positional Audio — everything below the master switch is hidden until it is on.
| Setting | Default | Description |
|---|---|---|
| Enable 3D Audio | false | Master switch |
| 3D Audio On by Default | false | Sets each new player's Earbuds default, inverted. On: Earbuds off, their radio is audible to others. Off: Earbuds on, their radio emits nothing. Never overrides a choice a player has made |
| Vehicle 3D Audio | true | A vehicle the player has been in keeps rebroadcasting their radio |
| Vehicle 3D Distance | 3 m | How far the player must walk before the vehicle takes over as the source |
| Player Speaker Range | 35 m | How far a handheld carries |
| Vehicle Speaker Range | 60 m | How far a vehicle-sourced radio carries |
Signal Coverage
| Setting | Default | Description |
|---|---|---|
| Always Full Signal | false | Force full bars and no degradation, without deleting your towers |
| Signal Degradation | false | Reduce audio quality with distance from the nearest tower |
| Falloff Rate | 24 | How fast quality drops. 0 gradual, 100 steep |
| Degradation Intensity | 50 | How severe degradation is at low signal |
Degradation does nothing until towers exist — see Signal towers.
Dispatch Scanning — Simultaneous Scan Audio (default false). Off, a transmission on the dispatcher's main channel mutes scanned channels. On, every scanned channel plays independently.
Radio Audio Effects — see Radio FX.
Keybinds
The radio key shows a hidden radio, focuses a visible one, and hides a focused one. Escape returns control to the game. Rebind in FiveM → Settings → Key Bindings → FiveM.
| Key binding | Default |
|---|---|
| Radio - Push to Talk | B |
| Radio - Open / Hide Radio | F6 |
| Radio - Voice Volume Up / Down | = / - |
| Radio - Tone Volume Up / Down | ] / [ |
| Radio - Power On/Off | unbound |
| Radio - Hide Radio | unbound |
| Radio - Emergency (hold) | unbound — hold for the codeplug's button hold time |
| Radio - Light / Dimmer | unbound — follows the radio's Light or Dimmer |
| Radio - Menu Arrow Up / Down / Left / Right | unbound |
| Radio - Home Button | unbound |
| Radio - Softkey 1 / 2 / 3 | unbound |
| Radio - Channel Up / Down, Zone Up / Down | unbound |
| Radio - Next / Previous Radio Model | unbound — hidden unless Radio Animations is on |
| Radio - 3D Volume Up / Down | unbound — hidden unless 3D Audio is on |
Advanced tab
Earpiece / Items — GTA outfit slots that switch earbuds on automatically. Clothing is checked every 7 seconds; a manual toggle holds until it changes.
| Field | Description |
|---|---|
| Type | prop for accessories, component for clothing |
| Slot | The GTA slot — props include Hat/Helmet (0), Glasses (1), Ear/Earpiece (2), Watch (6), Bracelet (7) |
| Drawable ID | The 0-based clothing variant index |
Networking & Limits — Control Message Rate Limit (default 120, range 30–2000). Control-plane messages per connection per 10 seconds — joins, leaves, scan edits, PTT starts. Voice is never counted.
LB Phone / LB Radio — see LB Phone and LB Radio. CAD / MDT — see CAD / MDT.
Radio layouts
Each radio model is a folder in layouts/, its screen described by ui.html using data- attributes. None of it changes channels, menus or keys. Apply with restart tRadio.
ui.html attribute | What it does |
|---|---|
data-radio-default-scale | Starting size 0.5–1.5 for a player with no saved size. DRX-330 1.25, QRX-720 1.15, QRX-720H 0.8 |
data-radio-power-source | "vehicle" hides the battery icon. Set on HLX-120, HLX-120G, ZTR-540, ZTR-540S and KVD-880 |
data-fit-min (on a text element) | Smallest size text may shrink to. Longer text is shortened with … |
data-radio-persistent-message="tint" | Top-view style: an emergency or Man Down tints the glass instead of a message strip. QRX-720H uses it |
Everything else is ordinary CSS in the layout's ui.html. Copy a shipped model's folder and edit it.
Sounds
Admin Panel → Sounds. All changes broadcast instantly.
Tone catalog
Every tone is a named slot mapped to a WAV. Map them under Global Tone Mappings, and upload new WAVs from Marketplace → Sounds.
| Tone | Plays when |
|---|---|
PTT / PTT_TRUNKED / PTT_PATCHED | You key up — conventional / trunked / patched |
PTT_END / PTT_END_TRUNKED / PTT_END_PATCHED | You release |
TX_START / TX_START_TRUNKED / TX_START_PATCHED | Someone else keys up |
TX_END / TX_END_TRUNKED / TX_END_PATCHED | Someone else releases |
BEEP | Generic alert beep |
BONK | Transmission collision |
CHIRP | Short acknowledgment, also the low-battery chirp |
ECHO | Message received |
PANIC | A unit's emergency starts, and each unacknowledged alarm retry. Not for Silent |
EMERGENCY_ENTRY / EMERGENCY_EXIT | Your own emergency starts / ends |
EMERGENCY_REJECT | The emergency button on a channel with no emergency, or power-off blocked by keep-alive |
EMERGENCY_NO_ACK | The alarm ran out of retries without an acknowledgment |
EMERGENCY_NO_EMERGENCY | Repeats while an emergency is paused on a channel with no emergency |
CONSOLE_ACK | Four tones: a dispatcher acknowledged your emergency |
MAN_DOWN_CONTINUOUS | Repeating during the Man Down post-alert |
MENU_INACTIVE_EXIT | The menu timeout returned the radio to Home |
TOT_WARN / TOT_EXPIRED | Transmit time-out warning / time-out |
ALERT_A / ALERT_B / ALERT_C | Default alert-tone slots |
ALERT_SIGNAL3 | Signal 3 alert tone |
PRIORITY / PRIORITY_REPEAT | Priority alert activation and each repeat |
RING_OUT / RING_IN | Heard by a 911 caller while ringing / by dispatchers when a call arrives |
GUNSHOT | A player fires while transmitting (2D and 3D) |
EXPLOSION | A nearby explosion is picked up while transmitting (2D, 3D and dispatch) |
Per-model overrides replace one tone for one model, or drop WAVs at layouts/{MODEL}/sounds/{TONE_NAME}.wav.
GUNSHOT ships as bgShot.wav, the one slot not named after its file. Clearing the mapping restores it rather than silencing gunshots.
EXPLOSION defaults to explosion.wav. Networked explosions within 150 m of a transmitting player are picked up; client-local ones are not.
Adding a sound
Drop the file into layouts/sounds/ — no folder to create, no manifest to write. The server lists any .wav there on the Sounds page, without a restart.
layouts/sounds/
ptt.wav
transStart.wav
airhorn.wav <- yours.ogg and .mp3 play too and install from the marketplace, but a bare .ogg dropped in by hand is not auto-discovered.
Background sounds
These fixed-filename WAVs in layouts/sounds/ play during a received transmission and cannot be renamed:
| File | Purpose |
|---|---|
transStart.wav | Click-in at the start of a received transmission |
transMid.wav | Looping background noise during it |
transEnd.wav / transEnd1.wav / transEnd2.wav | Click-out — one of the three at random |
bgSiren.wav | Siren loop |
bgHeli.wav | Helicopter rotors |
bgDog.wav | K9 |
bgShot.wav is not here: it is the stock file behind the remappable GUNSHOT slot.
Background Sound Modes maps what Config.bgModeCheck returns to WAV files. Three ship: siren, helicopter, k9; unmapped modes play nothing. For a WAV per siren tone, return distinct modes:
if tone == 2 then return "sirenWail" end
if tone == 3 then return "sirenYelp" end
if tone > 0 then return "siren" end| Provider | Where the tone index is |
|---|---|
| tELS | bag['tels:sirenTone'] on Entity(vehicle).state |
| LVC | state_lxsiren — 1 Airhorn, 2 Wail, 3 Yelp, 4 Priority, 5+ custom |
| LVC Fleet | Set per vehicle in the VCF config files |
| ELS-FiveM | newstate — 1–3 are audible |
Siren detection
Which ELS resource bgModeCheck reads to decide a siren is on.
| Provider | Value |
|---|---|
| Auto — try each installed provider in order | auto (default) |
| tELS only | tels |
| Luxart Vehicle Control only | lvc |
| LVC Fleet only | lvc_fleet |
| ELS-FiveM only | els_fivem |
| JLS only | jls |
| Advanced Lighting System only | als |
| Native GTA siren only | native |
Skip detection — implement bgModeCheck yourself | custom |
Auto order is tELS → LVC / LVC Fleet → ELS-FiveM → JLS → ALS → native. Each returns "not my vehicle" for a car it does not know, so mixed fleets work. Force a re-poll with exports['tRadio']:NotifySirenChanged().
Radio FX
The DSP chain applied to incoming radio voice: mic → input gain → highpass → lowpass → compressor → limiter → saturation → mid EQ → output.
| Setting | Default | Range |
|---|---|---|
| Radio FX | true | Master switch |
| Distortion Mode | classic | classic hard-clip, tube smooth saturation |
| Highpass Cutoff | 250 Hz | 80–800 |
| Lowpass Cutoff | 3500 Hz | 1200–8000 |
| Compression | 60 | 0–100 |
| Distortion | 30 | 0–100 |
| Mid Boost | 2 dB | −12 to 12 |
| Input Gain | 1.65 | 0.5–3.0 |
| Problem | Fix |
|---|---|
| Too muffled, telephone-like | Raise Lowpass Cutoff to 4000–5000 |
| Too tinny or harsh | Lower Highpass Cutoff to 150–200 |
| Volume pumping | Lower Compression |
| Crackling or clipped | Lower Input Gain to 0.8–1.2, or lower Distortion |
Digital is separate and per-channel — there is no global P25 switch. Set Mode to P25 Digital and System type to IMBE or M17 codec2. The analog chain applies on top, and the session's first transmission has a brief warm-up.
Update server and clients together. A client from an earlier release decodes the new IMBE frames with the old engine and hears distorted, overloud audio.
About Codec 2. The M17 system type is Codec 2, copyright David Rowe and contributors, under the LGPL 2.1. It is a separate, replaceable module, and nothing else in tRadio is covered by that license.
Alerts
Admin Panel → Alerts. An alert banners every radio on the channel, and the first alert in the list is what the in-game Alrt key fires. Each has three phases with optional tones: activate, repeat (persistent only), deactivate.
| Field | Default | Description |
|---|---|---|
| Name | — | Display text on the radio |
| Color | — | Hex color of the banner |
| Persistent | false | Stays active until manually cleared |
| Tone | — | Single tone used for all phases |
| Multi-tone | off | Switches to separate activate / repeat / deactivate tones |
| Repeat every | random 5–10 s | Gap between repeats. Minimum 1000 ms |
| Show banner on repeat | true | Flash the banner on each repeat |
| Deactivate label | RESUME | Banner text shown on clearance |
| Deactivate color | #126300 | Color of the clearance banner |
| Tone Only | false | Play the tone with no visual banner |
| Channel filter | all channels | Restrict this alert to specific channels |
Four ship by default: SIGNAL 100, SIGNAL 3, Ping and Bonk.
Speakers
Admin Panel → Coverage → Speakers. World-placed tone-board speakers a dispatcher can broadcast to. Grab coordinates with /tradio_debug coords; dispatchers use the Speakers button (see Working the board).
| Field | Default | Description |
|---|---|---|
| Position (X, Y, Z) | — | World coordinates |
| Heading | 0 | Prop rotation in degrees. Only affects the visible prop |
| Radius | 60 m | Audible reach, and who receives the tone. Applies from the next tone |
| Prop Model | none | Optional. Blank places no prop; the speaker still plays from its coordinates |
| Group | none | Speakers sharing a group name are one selectable target on the panel |
Signal towers
Admin Panel → Coverage → Towers. Signal strength falls off with distance from the nearest tower, so coverage is a union of reaches, not a sum. Leave gaps where players should lose signal.
| Field | Description |
|---|---|
| Name | Label in the panel |
| Position (X, Y, Z) | World coordinates |
| Prop model | Optional. Blank places no prop; the tower still provides coverage |
Integrations
LB Phone and LB Radio
Fully managed in Settings → Advanced. Nothing to configure in config.lua, and the two are independent.
| Setting | Default | Description |
|---|---|---|
| LB Phone 911 Dial-In | Off | Players dial 911 from the in-game phone and get call status as notifications. Requires lb-phone |
| LB Radio Frequency Mirror | Off | Mirrors frequency, connection status and user count into LB Phone's Radio app. Read-only. Needs lb-radioapp or lb-radio started |
The dial-in carries a restart badge: each player gets one 911 entry point on connect, and FiveM cannot un-register a chat command mid-session. The mirror applies live.
CAD / MDT
Fully managed in Settings → Advanced, and both toggles apply live.
| Setting | Default | Description |
|---|---|---|
| Mirror 911 Calls Into Your CAD | true | Every accepted 911 call becomes a CAD record with the caller's location |
| Mirror Panic Into Your CAD | true | Raise an emergency in your CAD when a unit hits their panic button |
There is nothing to install or key in. LACORE, SonoranCAD, ImperialCAD, wasabi_mdt, tk_mdt and ps-mdt are detected from the running resource. What each supports is listed under AI Dispatcher → CAD.
Panic is a call. Every CAD with a panic concept implements it as a call carrying an urgency marker. Those with a dedicated officer-in-distress alert get that instead.
ImperialCAD needs its resource started under exactly that name, with its own imperial_community_id and imperialAPI convars. A mirrored call is deleted only if it dies unanswered.
911 hooks
Two optional hooks in config/sv_functions.lua, both with sensible defaults.
-- The caller ID a dispatcher sees. A real 911 dispatcher only gets a number, so this —
-- not getPlayerName — is what the call card shows. Default: the caller's real LB Phone
-- number, otherwise a stable 555-XXXX placeholder derived from their license.
Config.getCall911CallerNumber = function(serverId)
return "555-0142"
end
-- Fires when a call is placed while no dispatch panel is connected. The call still queues.
-- 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
endMulti-server federation
Run several game servers against one shared voice server and dispatch panel.
- Host (Server A) runs the voice backend and must set
exposeWeb = true - Satellites run no backend. They federate player, panic, tone and alert state to the host, needing outbound access only
-- host
Config = {
exposeWeb = true,
isSatelliteServer = false,
hostServerUrl = "", -- must stay empty on the host
authToken = "<20+ random characters>",
}
-- each satellite
Config = {
hostServerUrl = "http://203.0.113.10:7777", -- the host's public URL, port included
isSatelliteServer = true,
authToken = "<same value as the host>",
}A satellite with an empty hostServerUrl is rejected at startup with isSatelliteServer=true requires hostServerUrl to be set to the host server's URL.
Satellites pull config every 30 seconds, and 911 calls need the host's voice server.
Next steps
- Show your players and dispatchers how to work it: Usage
- Drive tRadio from another resource: API Reference
- Restyle the dispatch panel: Dispatch panel theming
- A setting is not doing what this page says: Troubleshooting
Tommy's Radio
Still stuck? Support can answer it directly.
Was this page helpful?
TIMMYG Studios