Setup & Configuration
Complete installation and configuration guide for Tommy's Radio system.
Quick Start
Prerequisites
- FiveM Server
- Firewall configuration access
- Port available for voice server (default: 7777)
Installation Steps
1. Extract Files
- Download the radio resource files
- Extract to your server's
resourcesfolder - Ensure the folder is named
radio
Important
Keep the resource name as "radio" for proper export functionality.
2. Basic Configuration
Edit config.lua with your basic settings:
Config = {
-- Network Configuration
serverPort = 7777, -- Choose an unused port
authToken = "your-secure-token-123", -- Change this!
dispatchNacId = "141", -- Dispatch access code (can trigger first alert)
-- Default Controls
controls = {
talkRadioKey = "B",
toggleRadioKey = "F6",
},
-- Audio Settings
voiceVolume = 60, -- 0-100
sfxVolume = 20, -- 0-100
}3. Firewall Setup
Open the configured port on your server:
sudo ufw allow 7777netsh advfirewall firewall add rule name="Radio Voice Server" dir=in action=allow protocol=TCP localport=7777Environment Specific
Firewall and network configuration varies significantly between hosting environments, operating systems, and server providers. We do not provide specific support for these setups as they are outside the resource scope. Many online tutorials and resources are available for your specific environment.
4. Start Resource
Add to server.cfg:
start radio5. Setup Dispatch Panel
Option 1: Desktop App (Recommended)
Due to browser limitations — including the requirement for a secure (HTTPS) domain to enable microphone access and the lack of support for global hotkeys — we offer a native Windows desktop application which provides full functionality.
Download available on the main page dispatch panel section.
Benefits:
- No browser limitations
- Global hotkeys support
- Full microphone access
- Better performance
Option 2: Web Browser
Access via: http://your-server-ip:your-port
Browser Limitations
Most browsers block microphone access on HTTP. You'll need HTTPS or use browser flags for local development.
For Development/Testing:
- Use the
unsafely-treat-insecure-origin-as-secureChrome flag - Set flag for your dispatch panel URL
For Production:
- Use HTTPS with SSL certificate
- Use reverse proxy with domain
SSL/HTTPS Setup
For production environments, you'll want to setup HTTPS to enable full microphone functionality in browsers. This can be done through SSL certificates for your IP address, or by using a reverse proxy (like Nginx/Apache) with a domain and SSL certificate. This setup varies significantly by hosting provider and environment - there are many online tutorials available for your specific setup.
Framework Integration
NAC ID System
The permission system uses NAC (Network Access Code) IDs to determine user access levels.
NAC ID System
NAC IDs control which zones/channels users can access and what permissions they have (Signal 100, control frequencies, etc.).
Basic Integration
Config.radioAccessCheck = function(playerId)
-- Control who can access the radio system
-- Return true to allow access, false to deny
return true -- Allow everyone by default
endConfig.getUserNacId = function(serverId)
-- Example: Return NAC ID based on your framework
local player = YourFramework.GetPlayer(serverId)
if player and player.job then
if player.job.name == "police" then
return "100" -- Police NAC
elseif player.job.name == "ambulance" then
return "200" -- EMS NAC
end
end
return "000" -- Civilian/Default
endFramework Examples
QBCore Example
Radio Item Requirement (Optional)
You can configure the radio to require a specific item in the player's inventory. This example shows how to set up a radio item requirement with QBCore.
Optional: Add Radio Item to qb-core/shared/items.lua
['radio'] = {
['name'] = 'radio',
['label'] = 'Radio',
['weight'] = 1000,
['type'] = 'item',
['image'] = 'radio.png',
['unique'] = false,
['useable'] = true,
['shouldClose'] = true,
['combinable'] = nil,
['description'] = 'Handheld radio transceiver'
},Place an image file named radio.png in your qb-inventory/html/images/ folder (you can use any image of your choosing).
Configuration Examples:
-- Example 1: Require radio item in inventory
Config.radioAccessCheck = function(playerId)
local QBCore = exports['qb-core']:GetCoreObject()
local Player = QBCore.Functions.GetPlayer(playerId)
return Player and Player.Functions.GetItemByName("radio") ~= nil or false
end
-- Example 2: Job-based access (no item required)
Config.radioAccessCheck = function(playerId)
local player = exports['qb-core']:GetPlayer(playerId)
if player and player.PlayerData.job then
local job = player.PlayerData.job.name
return job == "police" or job == "ambulance" or job == "ems"
end
return false
end
-- Example 3: NAC ID based on job
Config.getUserNacId = function(serverId)
local player = exports['qb-core']:GetPlayer(serverId)
if player and player.PlayerData.job then
local job = player.PlayerData.job.name
if job == "police" then return "141"
elseif job == "ambulance" or job == "ems" then return "200"
elseif job == "firefighter" then return "300"
end
end
return "0"
end
-- Example 4: Display callsign or lastname
Config.getPlayerName = function(serverId)
local player = exports['qb-core']:GetPlayer(serverId)
if player and player.PlayerData then
if player.PlayerData.metadata.callsign then
return player.PlayerData.metadata.callsign
end
if player.PlayerData.charinfo.lastname then
return player.PlayerData.charinfo.lastname
end
end
return GetPlayerName(serverId)
end
-- Example 5: Make radio useable from inventory (optional)
-- Add to bottom of config.lua:
if IsDuplicityVersion() then
local QBCore = exports['qb-core']:GetCoreObject()
QBCore.Functions.CreateUseableItem('radio', function(source)
TriggerClientEvent('radio:use', source)
end)
else
RegisterNetEvent('radio:use', function()
exports['radio']:openRadio()
end)
endQBCore Example Config
Download a pre-configured example config with common QBCore integration patterns:
- Radio item requirement check
- Useable item integration (use from inventory)
- Job-based NAC ID assignment (Police, EMS, Fire)
- Callsign and lastname display support
- All v3.0 features included
Note: This is just an example - customize it for your server's needs!
ESX Example
Config.getUserNacId = function(serverId)
local xPlayer = ESX.GetPlayerFromId(serverId)
if xPlayer then
local job = xPlayer.getJob().name
if job == "police" then return "100"
elseif job == "ambulance" then return "200"
end
end
return "000"
endZone & Channel Configuration
Basic Zone Structure
Config.zones = {
[1] = {
name = "Statewide",
nacIds = { "100", "200" }, -- Who can access this zone
Channels = {
[1] = {
name = "DISPATCH",
type = "conventional",
frequency = 154.755,
allowedNacs = { "100" },
gps = {
color = 54,
visibleToNacs = { "100" }
}
}
}
}
}Channel Types
Conventional Channels
{
name = "DISPATCH",
type = "conventional",
frequency = 154.755,
allowedNacs = { "100" },
scanAllowedNacs = { "110" },
gps = { color = 54, visibleToNacs = { "100" } }
}Trunking Channels
{
name = "CAR-TO-CAR",
type = "trunked",
frequency = 856.1125, -- Control frequency
frequencyRange = { 856.000, 859.000 }, -- Available range
coverage = 500, -- Meters
allowedNacs = { "100" },
gps = { color = 25, visibleToNacs = { "100" } }
}Permission System
Control who can access the radio system using the radioAccessCheck function. Here are some example implementations:
-- Example 1: Allow everyone (default)
Config.radioAccessCheck = function(playerId)
return true
end
-- Example 2: QBCore - Require radio item
Config.radioAccessCheck = function(playerId)
local QBCore = exports['qb-core']:GetCoreObject()
local Player = QBCore.Functions.GetPlayer(playerId)
return Player and Player.Functions.GetItemByName("radio") ~= nil or false
end
-- Example 3: QBCore - Job-based access
Config.radioAccessCheck = function(playerId)
local player = exports['qb-core']:GetPlayer(playerId)
if player and player.PlayerData.job then
local job = player.PlayerData.job.name
return job == "police" or job == "ambulance" or job == "ems"
end
return false
endCustom Alerts Configuration
Configure custom alerts that can be triggered from dispatch or in-game:
-- Alerts configuration, the first alert is the default alert for the SGN button in-game
alerts = {
[1] = {
name = "SIGNAL 100", -- Alert Name
color = "#d19d00", -- Hex color code for alert
isPersistent = true, -- If true, the alert stays active until cleared
tone = "ALERT_A", -- Corresponds to a tone defined in client/radios/default/tones.json
},
[2] = {
name = "SIGNAL 3",
color = "#0049d1", -- Hex color code for alert
isPersistent = true, -- If true, the alert stays active until cleared
tone = "ALERT_A", -- Corresponds to a tone defined in client/radios/default/tones.json
},
[3] = {
name = "Ping",
color = "#0049d1", -- Hex color code for alert
tone = "ALERT_B", -- Corresponds to a tone defined in client/radios/default/tones.json
},
[4] = {
name = "Boop",
color = "#1c4ba3", -- Hex color code for alert
toneOnly = true, -- If true, only plays tone without showing alert on radio
tone = "BONK", -- Corresponds to a tone defined in client/radios/default/tones.json
},
},Alert Configuration Notes
- The first alert in the array is triggered by the SGN button in-game
- Users with the
dispatchNacIdcan trigger the first alert from in-game isPersistentalerts stay active until manually clearedtoneOnlyalerts play sound without visual notification
Custom Tones Configuration
Configure and customize alert tones using frequency-based tone generation:
{
"BEEP": [{ "freq": 910, "duration": 250 }],
"BONK": [{ "freq": 700, "duration": 300 }],
"ALERT_A": [
{ "freq": 600, "duration": 150, "delay": 200 },
{ "freq": 600, "duration": 150 }
],
"ALERT_B": [{ "freq": 500, "duration": 200 }],
"CUSTOM_TONE": [
{ "freq": 800, "duration": 100 },
{ "freq": 1000, "duration": 100, "delay": 50 },
{ "freq": 1200, "duration": 200 }
]...
}Creating Custom Tones
Tones are defined as arrays of frequency objects with the following properties:
freq- Frequency in Hz (20-20000)duration- How long the tone plays in millisecondsdelay- Optional delay before playing the next tone (milliseconds)
Radio-Specific Tone Overrides
You can override tones for specific radio types by placing a tones.json file in individual radio layout folders:
client/radios/
├── default/
│ └── tones.json (Global default tones)
├── ATX-8000/
│ └── tones.json (Overrides for ATX-8000 radio)
└── XPR-6500/
└── tones.json (Overrides for XPR-6500 radio)Tone Configuration
- Global tones are defined in
client/radios/default/tones.json - Radio-specific overrides take priority over global defaults
- Tone names in alerts configuration must match JSON keys
Advanced Settings
Audio Configuration
Config = {
-- Volume Defaults
voiceVolume = 60, -- Voice chat volume (0-100)
sfxVolume = 20, -- Radio effects volume (0-100)
-- Sound Effects
playTransmissionEffects = true, -- Background sounds (sirens, guns)
analogTransmissionEffects = true, -- Static/analog effects
-- 3D Audio (Experimental)
default3DAudio = false, -- Enable nearby radio audio
default3DVolume = 50, -- 3D audio volume (0-100)
-- PTT Settings
pttReleaseDelay = 350, -- Delay before releasing PTT (ms)
}Interference System
Config = {
bonkingEnabled = true, -- Enable radio interference
bonkInterval = 750, -- Interference interval (ms)
interferenceTimeout = 5000, -- How long interference lasts
blockAudioDuringInterference = true, -- Block voice during interference
}Background Siren Detection (LVC)
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 Integration: Check if siren AUDIO is actually playing
-- lvcSirenState > 0 means siren audio (Wail/Yelp/Priority/etc) is active
-- lvcSirenState = 0 means lights-only mode (no audio)
return lvcSirenState and lvcSirenState > 0
end,Standalone (Non Luxart Vehicle Control)
If you are not using Luxart Vehicle Control (LVC), you can implement your own siren detection logic.
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
-- Check if vehicle has sirens on
if not IsVehicleSirenOn(vehicle) then return false end
-- Check speed (convert m/s to mph) - lowered from 50 to 10 mph for better detection
local speed = GetEntitySpeed(vehicle) * 2.237
if speed <= 10 then return false end
-- Fallback: Just check if siren is on (will return true for lights-only mode too)
return IsVehicleSirenOn(vehicle)
end,Player Animations
Configure multiple animation styles that users can select through radio settings:
-- Multiple Animation Configurations
-- Users can select which animation to use through radio settings
animations = {
[1] = { -- No animations
name = "None",
onKeyState = function(isKeyDown)
-- Empty function for no animations
end,
onRadioFocus = function(focused)
-- Empty function for no animations
end
},
[2] = { name = "Shoulder", ... }, -- Police shoulder radio
[3] = { name = "Handheld", ... }, -- Physical radio prop
[4] = { name = "Earpiece", ... }, -- Earpiece/headset style
},Each animation configuration includes:
name- Display name for the animation styleonKeyState(isKeyDown)- Function called when PTT key is pressed/releasedonRadioFocus(focused)- Function called when radio UI is opened/closed (receives boolean)
Animation Types Available
- None: Disables animations completely
- Shoulder: Uses police shoulder radio animation (
random@arrests) - Handheld: Creates a physical radio prop with cellphone animation
- Earpiece: Simulates earpiece/headset usage with listen animation
Example: Adding Custom Animations with RPEmotes
You can add custom animations to your radio system using resources like rpemotes. Here's a complete guide using custom LEO poses as an example:
Step 1: Download Custom Animations
- Download the custom LEO poses from GTA5-Mods
- Create a folder called
radio(or any name) in yourrpemotes/stream/directory - Place all the downloaded animation files (
.ycdfiles) into this folder
Step 2: Register Animations in RPEmotes
Add the following to rpemotes/client/AnimationListCustom.lua inside the CustomDP.Emotes = {} table:
["radio2"] = {
"random@arrests",
"radio_chatter",
"Radio 2",
AnimationOptions = {
EmoteLoop = true,
EmoteMoving = true,
}
},
["radiochest"] = {
"anim@cop_mic_pose_002",
"chest_mic",
"Radio Chest",
AnimationOptions = {
EmoteLoop = true,
EmoteMoving = true,
}
},
["earpiece"] = {
"cellphone@",
"cellphone_call_listen_base",
"Earpiece",
AnimationOptions = {
EmoteLoop = true,
EmoteMoving = true,
}
},After adding these animations to rpemotes, you must either restart your server or refresh and restart the rpemotes resource for the changes to take effect.
Step 3: Configure Tommy's Radio
Once the emotes are registered in rpemotes, you can use them in your radio animations configuration:
[5] = {
name = "Chest",
onKeyState = function(isKeyDown)
-- Use radio chest animation when PTT is pressed
if isKeyDown then
exports["rpemotes"]:EmoteCommandStart("radiochest", 0)
else
exports["rpemotes"]:EmoteCancel(true)
end
end,
onRadioFocus = function(focused)
-- Use idle animation when radio UI is open
if focused then
exports["rpemotes"]:EmoteCommandStart("wt", 0)
else
exports["rpemotes"]:EmoteCancel(true)
end
end
},[6] = {
name = "Handheld2",
onKeyState = function(isKeyDown)
-- Use alternative handheld animation
if isKeyDown then
exports["rpemotes"]:EmoteCommandStart("wt4", 0)
else
exports["rpemotes"]:EmoteCancel(true)
end
end,
onRadioFocus = function(focused)
if focused then
exports["rpemotes"]:EmoteCommandStart("wt", 0)
else
exports["rpemotes"]:EmoteCancel(true)
end
end
},Important Notes
- Replace
"rpemotes"inexports["rpemotes"]with your actual rpemotes resource name if different - The
EmoteCommandStart()function executes the emote command (e.g., "wt4", "radiochest") EmoteCancel(true)stops the current animationonKeyStateis triggered when pressing/releasing the PTT keyonRadioFocusis triggered when opening/closing the radio UI
Battery System Configuration
Configure how the radio battery system works:
-- Battery system configuration
-- This function is called every second to update the battery level
-- @param currentBattery: number - current battery level (0-100)
-- @param deltaTime: number - time since last update in seconds
-- @return: number - new battery level (0-100)
batteryTick = function(currentBattery, deltaTime)
local playerPed = PlayerPedId()
local vehicle = GetVehiclePedIsIn(playerPed, false)
if vehicle ~= 0 then
-- Charge battery when in vehicle
local chargeRate = 0.5 -- 0.5% per second
return math.min(100.0, currentBattery + (chargeRate * deltaTime))
else
-- Discharge battery when on foot
local dischargeRate = 0.1 -- 0.1% per second
return math.max(0.0, currentBattery - (dischargeRate * deltaTime))
end
end,Battery System
- Customize how battery charges/discharges based on player state
- Can implement different rates for different scenarios
Custom Radio Layouts
Available Layouts
Config.radioLayouts = {
"AFX-1500", -- Mobile radio
"AFX-1500G", -- Marine mobile
"ARX-4000X", -- Compact handheld
"XPR-6500", -- Professional mobile
"XPR-6500S", -- Slim mobile
"ATX-8000", -- Advanced handheld
"ATX-8000G", -- Government handheld
"ATX-NOVA", -- Futuristic handheld
"TXDF-9100", -- Aviation radio
}Auto Layout Assignment
-- Default layouts by vehicle type and spawn code
defaultLayouts = {
["Handheld"] = "ATX-8000", -- On foot (required)
["Vehicle"] = "AFX-1500", -- In ground vehicles (required)
["Boat"] = "AFX-1500G", -- In boats (required)
["Air"] = "TXDF-9100", -- In aircraft (required)
-- Vehicle-specific overrides by spawn code
["fbi2"] = "XPR-6500", -- FBI vehicles use XPR-6500 layout
}Layout Assignment
- The first four vehicle types (Handheld, Vehicle, Boat, Air) are required
- You can override the default layout for specific vehicle spawn codes
- Vehicle-specific overrides take priority over vehicle type defaults
Creating Custom Layouts
- Copy existing layout folder from
client/radios/ - Rename to your custom name
- Replace assets:
radio.png- Main radio imageicons/folder - Button icons- Update
config.jsonwith positioning
Maintenance
Backup These Files
config.lua- Main configuration- Custom radio layouts in
client/radios/
Update Process
- Backup current configuration
- Extract new resource files
- Restore configuration settings
- Test before going live
Security Reminder
Always change authToken and dispatchNacId from defaults before production use!