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.
Understanding Port Requirements
Your server needs THREE separate ports:
- FiveM Server Port (usually 30120) - Players connect to your game server
- txAdmin Port (usually 40120) - For server management panel
- Radio Voice Server Port (7777 or any unused port) - THIS IS NEW AND REQUIRED
The radio runs its own voice communication server that needs its own dedicated port. You cannot use your existing FiveM or txAdmin ports. This is the #1 setup challenge for web panel hosting users.
2. Basic Configuration
Edit config.lua with your basic settings:
Config = {
-- Network Configuration
serverPort = 7777, -- Port for voice server (must be open/unused)
authToken = "CHANGE_ME_TO_RANDOM_TEXT", -- YOU CREATE THIS - any random secure string
dispatchNacId = "CHANGE_ME", -- YOU CHOOSE THIS - your dispatch panel password
-- Default Controls
controls = {
talkRadioKey = "B",
toggleRadioKey = "F6",
},
-- Audio Settings
voiceVolume = 60, -- 0-100
sfxVolume = 20, -- 0-100
}Understanding authToken
What is it? A security key that encrypts communication between your dispatch panel and server.
Where do I get it? You don't "get" it - YOU CREATE IT. Choose any random string of letters, numbers, and symbols.
Examples of good authTokens:
"myServer_SecureKey_2024!""RandomString123XYZ""Phoenix_PD_Radio_Token_987"
Why change it? If everyone uses the default "changeme", it's easy for others to crack your system. Make it unique to your server!
Understanding dispatchNacId
What is it? The password/code to access your dispatch panel.
Where do I get it? You don't "get" it - YOU CHOOSE IT. This is YOUR password that you're setting.
Examples:
"141"(default, but you can change it)"DISPATCH2024""MyServerPassword""911"
How to use it: When you open your dispatch panel (at http://yourip:port/), you'll be prompted to enter this NAC ID to login. Anyone who knows this code can access your dispatch panel.
Security tip: Change it from the default "141" to something unique to your server.
3. Firewall Setup
The radio system requires an additional open port for the voice server. This is separate from your FiveM server port and txAdmin port.
Why This Is Required
The radio runs its own voice server on your game server that handles all voice communication. Players connect to this voice server (on the port you configure) to transmit and receive audio. If the port is blocked, players will see "Failed to connect to radio server" errors.
Web Panel Hosting (Pterodactyl/Most Providers)
If you use a hosting provider with a web panel (Gravel Host, RocketNode, VibeGames, etc.), follow these steps:
Step 1: Locate Network/Port Settings
In your server's web panel, look for a section called:
- Network or Network Settings
- Ports or Allocations
- Additional Ports
You should see your existing ports:
- One for FiveM (usually 30120)
- One for txAdmin (usually 40120)
Step 2: Add a New Port
Look for a button like:
- "Create Allocation"
- "Add Port"
- "Request Additional Port"
Click it to add a new port. The panel will assign you a port number (e.g., 7777, 26162, 50978, etc.).
Important
You CANNOT use your existing FiveM or txAdmin ports. Each service needs its own dedicated port. The radio voice server must have its own separate port.
Step 3: Configure the Radio
Once you have the new port number, update your config.lua:
Config = {
serverPort = 50978, -- Use YOUR assigned port number here
connectionAddr = "http://YOUR_SERVER_IP:50978/", -- Include the port in the URL
-- ... rest of config
}Example with real values:
Config = {
serverPort = 50978,
connectionAddr = "http://192.0.2.100:50978/",
}Step 4: Restart the Resource
Restart your server or the radio resource from the panel.
Step 5: Verify the Port is Open
- Go to https://portchecker.co/check-v0
- Enter your server IP and the radio port (e.g.,
192.0.2.100:50978) - Click "Check Port"
✅ If it shows "Open" - You're all set! The radio should work in-game.
❌ If it shows "Closed" - Continue to troubleshooting below.
Step 6: Troubleshooting Closed Ports
If the port shows as closed after adding it in the panel:
- Try restarting your entire server from the panel (not just the resource)
- Check if the resource started successfully - Look for errors in console
- Contact your hosting provider - Many providers require manual port opening
Common Issue with Web Panels
Many Pterodactyl-based hosting providers require you to open a support ticket to actually open the port, even after adding it in the panel. This is a common limitation with these hosting environments.
When opening a ticket, tell them: "I need port [YOUR_PORT] opened for a custom voice server resource. The port is added in my panel but still shows as closed on port checkers."
VPS/Dedicated Server (Direct Access)
If you have SSH/console access to your server:
Ubuntu/Debian (UFW):
sudo ufw allow 7777CentOS/RHEL (firewalld):
sudo firewall-cmd --permanent --add-port=7777/tcp
sudo firewall-cmd --reloadWindows Server:
netsh advfirewall firewall add rule name="Radio Voice Server" dir=in action=allow protocol=TCP localport=7777After opening the port, you can usually leave connectionAddr blank:
Config = {
serverPort = 7777,
connectionAddr = "", -- Script will auto-detect your IP
}Home Server / Port Forwarding
If running from home, you need to:
- Forward port 7777 (TCP) in your router settings
- Configure your router's port forwarding rules (similar to Minecraft server setup)
- Use your public IP address in
connectionAddr
Testing Your Setup
After configuration, check these indicators:
✅ Working:
- Port checker shows "Open"
- Console shows "System Ready" without errors
- Players can connect to radio in-game
- Dispatch panel accessible at
http://yourip:port/
❌ Not Working:
- Port checker shows "Closed"
- Console shows "Health check failed" or "ECONNREFUSED"
- Players see "Failed to connect to radio server"
- Cannot access dispatch panel
Common Mistakes to Avoid
❌ Using FiveM port (30120) or txAdmin port (40120) - These are already in use!
❌ Not restarting the server after adding port - Panel changes may need a full restart
❌ Mismatched ports - serverPort and connectionAddr must use the SAME port number
❌ Testing before resource starts - Port only opens when radio resource is running
❌ Assuming panel = opened - Adding a port in your panel doesn't always open it; test with portchecker.co
❌ Wrong connectionAddr format - Must be "http://IP:PORT/" (include http:// and trailing /)
4. Start Resource
Add to server.cfg:
start radio5. Setup Dispatch Panel
Access your panel at http://yourserverip:yourport/ after the resource starts.
Desktop App (Recommended)
Download available on the main page.
Configure Your Server Endpoint
The app defaults to the demo server. You MUST change the endpoint to your own server.
Setup Steps:
- Open app and login with
141(demo default) - Click settings cog (⚙️) at top-right
- Change Endpoint URL to:
http://192.0.2.100:7777/(use your actual IP and port) - Save - app will refresh
- Login with YOUR
dispatchNacIdfrom config.lua
Web Browser
Development: Use Chrome flag unsafely-treat-insecure-origin-as-secure for HTTP microphone access
Production: Setup HTTPS with SSL certificate or reverse proxy (Nginx/Apache with domain)
Browser Limitation
Browsers block microphone on HTTP. Desktop app bypasses this limitation.
Discord Authentication (Optional)
You can enable Discord OAuth to authenticate dispatchers instead of using the NAC ID password system.
Configuration Steps:
- In
config.lua, enable Discord auth:
useDiscordAuth = false, -- Set to true to enable Discord authentication- Create a
.envfile in theserverfolder of the resource:
# Discord Application Credentials
DISCORD_CLIENT_ID="your_discord_client_id"
DISCORD_SECRET="your_discord_secret"
DISCORD_GUILD_ID="your_discord_guild_id"
# Optional: Restrict access to specific roles (comma-separated Role IDs)
# Leave empty ("") to allow all server members
DISCORD_ROLES=""
# Optional: Override redirect URI for reverse proxy/domain setups
DISCORD_REDIRECT_URI=""- Get your Discord credentials from the Discord Developer Portal
- Add the redirect URI to your Discord Application's OAuth2 settings:
- Format:
http://your-server-url:port/radio/dispatch/auth - Example:
http://localhost:7777/radio/dispatch/auth
- Format:
Discord Setup
Getting Discord IDs: Enable Developer Mode in Discord (User Settings > Advanced) to copy Server IDs and Role IDs by right-clicking.
Role Restrictions: Leave DISCORD_ROLES empty to allow all server members, or add specific Role IDs (comma-separated) to restrict access.
Framework Integration
NAC ID System
NAC IDs control zone/channel access and permissions (Signal 100, control frequencies, etc.).
Core Functions:
-- Control who can use the radio
Config.radioAccessCheck = function(playerId)
return true -- Allow everyone by default
end
-- Assign NAC IDs based on job/role
Config.getUserNacId = function(serverId)
local player = YourFramework.GetPlayer(serverId)
if player and player.job then
if player.job.name == "police" then return "100"
elseif player.job.name == "ambulance" then return "200"
end
end
return "000" -- Default/Civilian
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.5 features included
Note: This is just an example - customize it for your server's needs!
ESX 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 ESX Legacy.
Optional: Add Radio Item to your database
INSERT INTO `items` (`name`, `label`, `weight`, `rare`, `can_remove`) VALUES
('radio', 'Radio', 1, 0, 1);Place an image file named radio.png in your inventory images folder (location varies by inventory system).
Configuration Examples:
-- ESX Integration (place at top of config.lua)
ESX = exports['es_extended']:getSharedObject()
-- Example 1: Require radio item in inventory
Config.radioAccessCheck = function(playerId)
local xPlayer = ESX.GetPlayerFromId(playerId)
if not xPlayer then return false end
local radioItem = xPlayer.getInventoryItem('radio')
return radioItem and radioItem.count > 0 or false
end
-- Example 2: Job-based access (no item required)
Config.radioAccessCheck = function(playerId)
local xPlayer = ESX.GetPlayerFromId(playerId)
if not xPlayer then return false end
local job = xPlayer.getJob()
return job.name == "police" or job.name == "ambulance" or job.name == "fire"
end
-- Example 3: NAC ID based on job
Config.getUserNacId = function(serverId)
local xPlayer = ESX.GetPlayerFromId(serverId)
if not xPlayer then return "0" end
local job = xPlayer.getJob()
if job.name == "police" then return "141"
elseif job.name == "ambulance" then return "200"
elseif job.name == "fire" or job.name == "firefighter" then return "300"
end
return "0"
end
-- Example 4: Display callsign or lastname
Config.getPlayerName = function(serverId)
if not serverId or serverId <= 0 then return "DISPATCH" end
local xPlayer = ESX.GetPlayerFromId(serverId)
if not xPlayer then return GetPlayerName(serverId) end
-- Check for callsign in metadata
local callsign = xPlayer.getMeta("callsign")
if callsign and callsign ~= "" then
return callsign
end
-- Get character name (returns firstname + lastname with esx_identity)
local characterName = xPlayer.getName()
if characterName and characterName ~= "" then
-- Extract last name
local lastName = string.match(characterName, "%s+(%S+)$")
if lastName then return lastName end
return characterName
end
return GetPlayerName(serverId)
end
-- Example 5: Make radio useable from inventory (optional)
-- Add to bottom of config.lua:
if IsDuplicityVersion() then
ESX.RegisterUsableItem('radio', function(playerId)
TriggerClientEvent('radio:use', playerId)
end)
else
RegisterNetEvent('radio:use', function()
exports['radio']:openRadio()
end)
endESX Example Config
Download a pre-configured example config with common ESX Legacy 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.5 features included
Note: This is just an example - customize it for your server's needs!
Zone & 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 Examples
Config.radioAccessCheck = function(playerId)
local Player = exports['qb-core']:GetCoreObject().Functions.GetPlayer(playerId)
return Player and Player.Functions.GetItemByName("radio") ~= nil or false
endConfig.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
alerts = {
[1] = {
name = "SIGNAL 100",
color = "#d19d00",
isPersistent = true, -- Stays until cleared
tone = "ALERT_A", -- From tones.json
},
[2] = {
name = "SIGNAL 3",
color = "#0049d1",
isPersistent = true,
tone = "ALERT_A",
},
[3] = {
name = "Ping",
color = "#0049d1",
tone = "ALERT_B",
},
[4] = {
name = "Boop",
color = "#1c4ba3",
toneOnly = true, -- Sound only, no visual
tone = "BONK",
},
},Notes:
- First alert triggered by SGN button in-game
- Users with
dispatchNacIdcan trigger first alert isPersistentkeeps alert until manually clearedtoneOnlyplays sound without visual notification
Custom Tones Configuration
Edit client/radios/default/tones.json:
{
"BEEP": [{ "freq": 910, "duration": 250 }],
"BONK": [{ "freq": 700, "duration": 300 }],
"ALERT_A": [
{ "freq": 600, "duration": 150, "delay": 200 },
{ "freq": 600, "duration": 150 }
],
"CUSTOM_TONE": [
{ "freq": 800, "duration": 100 },
{ "freq": 1000, "duration": 100, "delay": 50 },
{ "freq": 1200, "duration": 200 }
]
}Properties:
freq- Frequency in Hz (20-20000)duration- Tone length in millisecondsdelay- Optional delay before next tone (milliseconds)
Radio-Specific Overrides:
Place tones.json in client/radios/YOUR-MODEL/ to override defaults for that radio model.
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
Developer Tools
Radio Debug Mode
Command: /radio_debug (radio must be open)
Visual Overlays:
- Orange banner when active
- Red borders = Buttons | Blue borders = Displays | Green borders = Icons | Orange borders = LEDs
Click Tracking:
- Click anywhere to log coordinates
- Red dot appears and fades
- Format:
Click coordinates: x=123, y=456
Usage: Click top-left corner, note X/Y, then bottom-right to calculate width/height. All coordinates relative to PNG top-left (0,0).
{
"key": "power",
"x": 180, "y": 161,
"width": 25, "height": 25
}Radio Sound Test
Command: /rplay <sound_type> (radio must be on)
Available Sounds:
/rplay trans # Full transmission simulation
/rplay gunshot # Background gunshot
/rplay siren # Background siren (3s)
/rplay heli # Background helicopter (3s)
/rplay BEEP # Any tone from tones.json
/rplay ALERT_ACustom Tone Format:
{
"TONE_NAME": [
{ "freq": 900, "duration": 40, "delay": 60 },
{ "freq": 1200, "duration": 50 }
]
}freq: Frequency in Hz (100-4000)duration: Length in millisecondsdelay: Optional pause before next tone
Testing: Edit tones.json → bun run build → restart → /rplay YOUR_TONE_NAME
Override per radio: Place tones.json in client/radios/YOUR-MODEL/ to override defaults.
Reset Radio Position
Command: /resetRadio
Resets radio scale and position to default. Use this if you accidentally drag the radio off-screen in move mode or have positioning issues.
Maintenance
Backup These Files
config.lua- Main configuration- Custom radio layouts in
client/radios/
Update Process
- Check config version - Open your current
config.luaand note the version at the top:-- Config Version 3.3 - Backup current configuration
- Extract new resource files
- Compare config versions:
- If version is the same - Keep your existing
config.lua, no changes needed - If version is different - Use diffchecker.com to compare your old config with the new one and merge changes
- If version is the same - Keep your existing
- Restore custom settings (authToken, dispatchNacId, zones, channels, etc.)
- Test before going live
Config Version Check
Always check the config version comment at the top of config.lua. If it hasn't changed between updates, you can keep your existing configuration without modifications.
Security Reminder
Always change authToken and dispatchNacId from defaults before production use!