Configuration
Complete configuration reference for Tommy's Radio — config.lua, admin panel, zones, channels, alerts, and tones.
Configuration Overview
Tommy's Radio uses two configuration sources:
| Source | Contains | Edited via |
|---|---|---|
config.lua | Network, keybinds, zones/channels, alerts, callbacks | Text editor (requires restart) |
data.json | Audio, radio FX, bonking, callsigns, 3D audio, GPS rate, default layouts | In-game admin panel (live) |
Settings in data.json are managed through the admin panel and broadcast to all clients immediately — no restart needed.
config.lua Reference
Network & Authentication
Config = {
serverAddress = "", -- Connection URL sent to clients. Leave empty for auto-detect.
-- Set to "http://IP:PORT/" if using a proxy or specific IP.
serverPort = 7777, -- Port for voice server and dispatch panel
authToken = "changeme", -- Secret key for API authentication. Change this.
dispatchNacId = "141", -- Dispatch panel login password
}General
Config = {
checkForUpdates = true, -- Check for updates on resource start
healthCheck = true, -- Run port reachability check on start
logLevel = 3, -- 0=Error, 1=Warn, 2=Minimal, 3=Normal, 4=Debug, 5=Verbose
useDiscordAuth = false, -- Use Discord OAuth for dispatch panel (see Dispatch Panel page)
}Keybinds
Config.controls = {
talkRadioKey = "B", -- Push-to-talk
toggleRadioKey = "F6", -- Open/close radio
closeRadioKey = "", -- Close radio only (separate from toggle)
powerBtnKey = "", -- Power on/off
channelUpKey = "", -- Next channel
channelDownKey = "", -- Previous channel
zoneUpKey = "", -- Next zone
zoneDownKey = "", -- Previous zone
menuUpKey = "", -- Navigate menu up
menuDownKey = "", -- Navigate menu down
menuRightKey = "", -- Navigate menu right
menuLeftKey = "", -- Navigate menu left
menuHomeKey = "", -- Return to menu home
menuBtn1Key = "", -- Menu button 1 (context-dependent)
menuBtn2Key = "", -- Menu button 2 (context-dependent)
menuBtn3Key = "", -- Menu button 3 (context-dependent)
emergencyBtnKey = "", -- Panic / emergency button
styleUpKey = "", -- Next radio model
styleDownKey = "", -- Previous radio model
voiceVolumeUpKey = "", -- Voice volume up
voiceVolumeDownKey = "", -- Voice volume down
sfxVolumeUpKey = "", -- SFX volume up
sfxVolumeDownKey = "", -- SFX volume down
volume3DUpKey = "", -- 3D audio volume up
volume3DDownKey = "", -- 3D audio volume down
-- Leave empty ("") to disable a keybind
}Signal Towers
Positions for the signal-strength icon and optional audio degradation.
Config.signalTowerCoordinates = {
{ x = 1860.0, y = 3677.0, z = 33.0 },
{ x = 449.0, y = -992.0, z = 30.0 },
-- Add, remove, or move coordinates to match your map
}The radio shows more signal bars when closer to a tower. When signal degradation is enabled (via admin panel), distance also affects audio quality — further from towers means more garbled audio with random dropouts.
Battery
The batteryTick callback controls battery drain and charge rates.
Config.batteryTick = function(currentBattery, deltaTime)
local vehicle = GetVehiclePedIsIn(PlayerPedId(), false)
if vehicle ~= 0 then
return math.min(100.0, currentBattery + (0.5 * deltaTime)) -- Charge in vehicle
else
return math.max(0.0, currentBattery - (0.1 * deltaTime)) -- Drain on foot
end
endClient Callbacks
talkCheck
Runs before each PTT transmission. Return false to block.
Config.talkCheck = function()
if IsPlayerDead(PlayerId()) then return false end
if IsPedSwimming(PlayerPedId()) then return false end
-- Add custom conditions: handcuffs, tackle, etc.
return true
endbgSirenCheck
Determines if siren background SFX should play when a player transmits. Other players on the channel hear the siren looping behind the voice audio.
Return values:
| Return | Behavior |
|---|---|
false / nil | No siren audio |
true | Default siren (bgSiren.wav) |
"filename.wav" | Custom audio file from client/radios/default/sounds/ |
Config.bgSirenCheck = function(lvcSirenState)
local vehicle = GetVehiclePedIsIn(PlayerPedId(), false)
if vehicle == 0 then return false end
return lvcSirenState and lvcSirenState > 0
endCustom Siren Audio (LVC Example)
By returning a filename instead of true, you can play the actual siren tone matching what's active in-game. This requires siren WAV files placed in client/radios/default/sounds/ named by siren ID (e.g., 1.wav, 2.wav, 21.wav).
To use this with LVC, add @lvc/UTIL/cl_lvc.lua to your fxmanifest.lua client scripts, then modify bgSirenCheck:
Config.bgSirenCheck = function(lvcSirenState)
local playerPed = PlayerPedId()
if not playerPed or playerPed == 0 then return false end
local vehicle = GetVehiclePedIsIn(playerPed, false)
if not vehicle or vehicle == 0 then return false end
-- LVC: return the siren ID as a filename so the correct tone plays over radio
if state_lxsiren and state_lxsiren[vehicle] then
local sirenId = state_lxsiren[vehicle]
if sirenId and sirenId > 0 then
return tostring(sirenId) .. ".wav"
end
end
-- Fallback: default siren if native siren is on
if IsVehicleSirenOn(vehicle) then return true end
return false
endWith this setup, when a player has their wail siren active (e.g., siren ID 1), other players hear 1.wav over the radio instead of the generic siren loop. Switching to yelp (e.g., ID 21) plays 21.wav on the next transmission.
The siren audio updates between transmissions. If a player switches siren tones mid-transmission, the new tone takes effect the next time they key up.
Server Callbacks
These three functions run server-side and control who can use the radio and what they can access.
The string returned by getUserNacId is the player's NAC ID — the radio compares it against:
- Zone
nacIds— player can see the zone only if their NAC ID is in the list - Channel
allowedNacs— player can connect and transmit only if their NAC ID is in the list - Channel
scanAllowedNacs— player can scan (listen only) if their NAC ID is in the list
-- Gate: who can use the radio at all (return false to block)
Config.radioAccessCheck = function(playerId)
return true
end
-- Identity: NAC ID assigned to this player (matched against zones/channels)
Config.getUserNacId = function(serverId)
return "141"
end
-- Display name shown on the radio and in the dispatch panel
Config.getPlayerName = function(serverId)
return GetPlayerName(serverId)
endFor framework-specific examples (QBCore, ESX, Qbox job-based NAC IDs), see Framework Integration. When a player's job changes, call exports['tRadio']:refreshNacId(serverId) to update their access immediately.
ACE Permissions
As an alternative to the getUserNacId callback, you can assign permissions directly through FiveM ACE entries in server.cfg. ACE permissions are additive — they work alongside NAC IDs, so existing configs don't need to change.
NAC ID via ACE
Assign a NAC ID to a player or group without writing Lua code. If getUserNacId returns nil, the server checks for a matching tRadio.nac.* ACE:
# Give police group NAC ID "141"
add_ace group.police tRadio.nac.141 allow
add_principal identifier.steam:STEAM_ID group.police
# Give EMS NAC ID "110"
add_ace group.ems tRadio.nac.110 allowIf getUserNacId returns a value, it takes priority over ACE NAC IDs. To use ACEs exclusively, have getUserNacId return nil.
Per-Frequency ACE Permissions
Grant access to specific frequencies without a NAC ID. The frequency is encoded by removing the decimal point (e.g., 154.815 → 154815).
| ACE Permission | Grants |
|---|---|
tRadio.connect.{freq} | Transmit on a frequency |
tRadio.scan.{freq} | Scan (listen only) on a frequency |
tRadio.gps.{freq} | See GPS blips for a frequency |
tRadio.zone.{N} | Access zone N (1-based index from Config.zones) |
# EMS can transmit on 154.755 and 154.815
add_ace group.ems tRadio.connect.154755 allow
add_ace group.ems tRadio.connect.154815 allow
# EMS can scan 856.1125
add_ace group.ems tRadio.scan.8561125 allow
# EMS can see GPS blips on 154.755
add_ace group.ems tRadio.gps.154755 allow
# EMS can access zone 1 (Statewide)
add_ace group.ems tRadio.zone.1 allowAdmin Radio Features via ACE
Players with the tlib.admin ACE permission (same permission used for the in-game admin panel) gain access to additional radio controls — the SGN alert button and trunked CT/TK toggle.
add_ace group.admin tlib.admin allowCombining NAC IDs and ACE Permissions
A player's effective access is the union of their NAC ID permissions and ACE permissions. For example, a player with NAC ID "110" and tRadio.connect.154815 can access everything NAC "110" grants plus transmit on frequency 154.815.
When permissions change (e.g., a job update triggers refreshNacId), both NAC ID and ACE permissions are re-resolved and sent to the client. If a player loses access to their current channel, they are automatically disconnected.
Admin Panel (data.json)
Settings managed through the in-game admin panel. Access requires the tlib.admin ACE permission.
Grant it in server.cfg:
add_ace identifier.steam:STEAM_ID tlib.admin allowOr grant to a group:
add_ace group.admin tlib.admin allow
add_principal identifier.steam:STEAM_ID group.adminOpen via the /tradio command > Admin Settings.
Audio Settings
| Setting | Default | Description |
|---|---|---|
voiceVolume | 65 | Default voice volume (0-100) |
sfxVolume | 35 | Default SFX/tone volume (0-100) |
volumeStep | 5 | Volume adjustment increment |
pttReleaseDelay | 350 | Milliseconds to keep transmitting after releasing PTT |
pttTriggersProximity | true | PTT also triggers proximity voice |
Radio FX
| Setting | Default | Description |
|---|---|---|
radioFx.fxEnabled | true | Enable analog audio processing chain |
radioFx.p25Enabled | false | Enable P25 IMBE vocoder |
radioFx.distortion | 40 | Distortion amount |
radioFx.inputGain | 1.65 | Input gain multiplier |
radioFx.compression | 100 | Compression amount |
radioFx.midBoost | 3 | Mid-frequency boost (dB) |
radioFx.highpassFrequency | 300 | Highpass filter cutoff (Hz) |
radioFx.lowpassFrequency | 3000 | Lowpass filter cutoff (Hz) |
P25 and analog FX can be used independently or together. Using both stacks the analog chain on top of the vocoder output.
Transmission Effects
| Setting | Default | Description |
|---|---|---|
playTransmissionEffects | true | Play background SFX (sirens, gunshots, helicopters) |
analogTransmissionEffects | true | Apply analog processing to transmission effects |
Bonking
Controls what happens when a player tries to transmit while someone else is already talking.
| Setting | Default | Description |
|---|---|---|
bonking.playBonkTone | true | Play a tone when transmission is blocked |
bonking.bonkToneGlobal | true | All channel users hear the bonk tone |
bonking.blockTransmission | true | Block overlapping transmissions |
bonking.doubleTapOverride | true | Double-tap PTT to override and transmit anyway |
bonking.doubleTapWindow | 1500 | Time window (ms) for double-tap detection |
3D Audio
| Setting | Default | Description |
|---|---|---|
enable3DAudio | true | Enable 3D spatial audio system |
default3DAudio | true | 3D audio on by default for new players |
default3DVolume | 50 | Default 3D audio volume (0-100) |
vehicleRadio3DAudioEnabled | true | Radio plays near vehicle when player walks away |
vehicle3DActivationDistance | 1.0 | Distance from vehicle before vehicle radio activates |
Callsign System
| Setting | Default | Description |
|---|---|---|
useCallsignSystem | true | Enable the built-in callsign system |
callsignCommand | "callsign" | Chat command to set callsign (e.g., /callsign 2L-319) |
Earbud Clothing Detection
Automatically enable earbuds mode when a player wears specific clothing items (hats, earpieces, accessories). Configured through the admin panel — no restart required.
Each entry in the earbudItems list defines a clothing slot and drawable ID to match:
| Field | Type | Description |
|---|---|---|
slotId | number | GTA V component/prop slot ID |
slotType | string | "prop" for accessories/hats, "component" for clothing |
drawableId | number | The drawable index to match (use the 3D Prop Editor or GTA ped tools to find IDs) |
Behaviour:
- Checked every 7 seconds via a polling thread
- Only triggers on state change (wearing → not wearing or vice versa)
- If the player manually toggles earbuds, the auto-detection respects that override until the clothing state changes again
GPS & Signal
| Setting | Default | Description |
|---|---|---|
gpsBlipUpdateRate | 50 | GPS blip update interval (ms). Lower = smoother, higher CPU |
signalDegradationEnabled | false | Audio quality degrades with distance from signal towers |
signalDegradationIntensity | 50 | How aggressive signal degradation is (0-100) |
signalFalloffRate | 50 | How quickly signal drops with distance |
Emergency
| Setting | Default | Description |
|---|---|---|
panicTimeout | 60000 | Milliseconds before panic auto-clears |
Default Layouts
Default radio model per vehicle type. Configurable via admin panel or /tradio > Set as Default Layout for Vehicle.
| Vehicle Type | Default Model |
|---|---|
| Handheld | ATX-8000 |
| Vehicle | AFX-1500 |
| Boat | AFX-1500G |
| Air | TXDF-9100 |
Radio models are auto-discovered from client/radios/ subfolders.
Zones & Channels
Zones group channels together. Players see zones based on their NAC ID matching the zone's nacIds list.
Zone Structure
Config.zones = {
[1] = {
name = "Statewide",
nacIds = { "141", "200" }, -- NAC IDs that can see this zone
Channels = {
[1] = {
name = "DISPATCH",
type = "conventional",
frequency = 154.755,
allowedNacs = { "141" },
scanAllowedNacs = { "200" },
gps = {
color = 54,
visibleToNacs = { "141" },
},
},
},
},
}Channel Types
Conventional
Single shared frequency — all connected players hear each other regardless of location.
{
name = "DISPATCH",
type = "conventional",
frequency = 154.755,
allowedNacs = { "141" },
scanAllowedNacs = { "200" },
gps = { color = 54, visibleToNacs = { "141" } },
}Trunked
Location-based frequency assignment. Units at different locations get separate sub-frequencies automatically. Dispatchers can still broadcast to all units on the control frequency.
{
name = "CAR-TO-CAR",
type = "trunked",
frequency = 856.1125, -- Control frequency
frequencyRange = { 856.000, 859.000 }, -- Available range
coverage = 500, -- Radius in meters
allowedNacs = { "141" },
gps = { color = 25, visibleToNacs = { "141" } },
}Channel Field Reference
| Field | Type | Description |
|---|---|---|
name | string | Display name on the radio |
type | string | "conventional" or "trunked" |
frequency | number | Frequency in MHz (must be unique across all zones) |
frequencyRange | table | Trunked only — { min, max } range |
coverage | number | Trunked only — radius in meters |
allowedNacs | table | NAC IDs that can connect and scan |
scanAllowedNacs | table | NAC IDs that can scan only (not connect) |
gps.color | number | Blip color ID |
gps.visibleToNacs | table | NAC IDs that can see this channel's GPS blips |
Alerts
Alerts are defined in Config.alerts. The first entry ([1]) is the default triggered by the in-game SGN button. Players whose NAC ID matches dispatchNacId can trigger it from the radio; all alerts can also be triggered from the dispatch panel or API.
Tone Styles
Simple — one tone for all phases:
{
name = "SIGNAL 3",
color = "#0049d1",
isPersistent = true,
tone = "ALERT_A",
}Per-phase — independent tones for activate, repeat, and deactivate:
{
name = "SIGNAL 100",
color = "#d19d00",
isPersistent = true,
tones = {
activate = "PRIORITY",
["repeat"] = "PRIORITY_REPEAT",
deactivate = "ALERT_C",
},
}Any phase can be omitted — it falls back to the tone field, or plays nothing if absent.
Alert Field Reference
| Field | Type | Default | Description |
|---|---|---|---|
name | string | — | Display text on the radio |
color | string | — | Hex colour of the alert banner |
isPersistent | bool | false | Stays active until manually cleared |
tone | string | — | Fallback tone for all phases |
tones.activate | string | — | Tone played once on activation |
tones["repeat"] | string | — | Tone played on each repeat loop |
tones.deactivate | string | — | Tone played once on clearance |
repeatInterval | number (ms) | random 5-10s | Gap between repeat fires |
repeatShowBanner | bool | true | Flash the banner on each repeat |
deactivateLabel | string | "RESUME" | Banner text shown on clearance |
deactivateColor | string | "#126300" | Colour of the clearance banner |
toneOnly | bool | false | Play tone only, no visual banner |
Example
Config.alerts = {
[1] = {
name = "SIGNAL 100",
color = "#a38718",
isPersistent = true,
tones = {
activate = "PRIORITY",
["repeat"] = "PRIORITY_REPEAT",
deactivate = "ALERT_C",
},
repeatInterval = 15000,
repeatShowBanner = false,
deactivateLabel = "RESUME",
deactivateColor = "#1a8a38",
},
[2] = {
name = "SIGNAL 3",
color = "#1852a3",
isPersistent = true,
tone = "ALERT_A",
},
[3] = {
name = "Ping",
color = "#1852a3",
tone = "ALERT_B",
},
[4] = {
name = "Boop",
color = "#1c4ba3",
toneOnly = true,
tone = "BONK",
},
}Custom Tones
Tones are defined in client/radios/default/tones.json. Each key is the tone name (case-insensitive).
Oscillator Tones
Synthesised on the fly by the Web Audio API:
{
"BEEP": [{ "freq": 910, "duration": 250 }],
"ALERT_A": [
{ "freq": 600, "duration": 150, "delay": 200 },
{ "freq": 600, "duration": 150 }
]
}| Property | Description |
|---|---|
freq | Frequency in Hz (20-20000) |
duration | Length in milliseconds |
delay | Optional pause before this step (ms) |
WAV File Tones
Set a tone's value to an empty array [] to play a .wav file instead. The file name must match the tone key (lowercase).
{
"ECHO": [],
"SIREN": []
}"ECHO": [] looks for echo.wav in client/radios/default/sounds/. Supported formats: .wav, .mp3, .ogg (.wav recommended for lowest latency).
WAV tones work everywhere oscillator tones do: in-game radio, 3D spatial audio, and the dispatch panel.
Per-Model Overrides
Place a tones.json in client/radios/YOUR-MODEL/ to override individual tones for that model. Keys present take precedence; others fall back to client/radios/default/tones.json.
client/radios/
+-- default/
| +-- tones.json
| +-- sounds/echo.wav
+-- AFX-1500/
+-- tones.json (overrides specific tones)
+-- sounds/echo.wav (model-specific WAV)