Setup & Configuration

Prerequisites

  • FiveM Server

  • Firewall configuration access


Installation

1. Download and Extract

  1. Download the radio resource files

  2. Extract to your server's resources folder

  3. Open the fxmanifest.lua file in the resource folder.

  4. Make sure the fx_version is set to bodacious , otherwise it will not work!

fx_version 'bodacious'

2. Start the Resource

Add to server.cfg:

start radio

It is important to keep the resource name to "radio" for export usage.

3. Voice Server / Dispatch Panel Setup

The radio system includes its own voice server:

  1. Port: Default port 7777 (configurable in config.lua)

  2. Auto-start: Voice server starts automatically with the resource

  3. Access: Dispatch panel accessible at http://your-server-ip:your-port

4. Configure Firewall

Ensure the serverPort (as configured in config.lua) is open / exposed on the host server.

# Ubuntu/UFW
sudo ufw allow 7777

# Windows
netsh advfirewall firewall add rule name="Radio Voice Server" dir=in action=allow protocol=TCP localport=7777

This process may vary based on your server's operating system, yet should be fairly easy to perform.

4. Dispatch Panel

At this point, the radio should be functional, and the dispatch panel should be accessible at http://your-server-ip:your-port , however, most modern browsers block microphone access for insecure URLs. For this reason, we provide a desktop app, which supports global hotkeys and will work without further setup.

5. Web-Based Setup (Optional)

Since most modern browsers block microphone access for insecure URLs, there are two options:

Bypass Browser Block (Simple)

Most modern browsers allow users the ability to bypass the microphone access block on configured sites using the unsafely-treat-insecure-origin-as-secure flag. You could have your dispatchers set this flag for your dispatch panel site, and everything should work as normal.

Production Use (HTTPS Recommended) We recommend using HTTPS for dispatch panel microphone access. You have two options:

  1. SSL Certificate for IP

    • Get an SSL certificate for your IP address.

    • Set config.lua connection string to: https://your-server-ip:your-port

  2. Reverse Proxy with Domain

    • Use an SSL-certified domain (e.g., via Nginx or Apache) to proxy requests to the voice server/dispatch panel port.

    • Set config.lua connection string to: https://proxy.example.com

As many systems have different configurations & distributions, we do not provide support for setting this up - as this part is outside of the resource's scope. There are plenty of videos on YouTube that can assist with this process, and even AI tools like ChatGPT can help guide you through it.


Basic Configuration

config.lua

-- Network Configuration
connectionAddr = "",    -- Final connection string for clients, Can include protocol (https://proxy.example.com) and/or port. (If empty, uses host ip address and port)
serverPort = 7777,      -- Port for radio server & dispatch panel, choose a port that is not used by other resources.
authToken = "changeme", -- Secure token for radio authentication, change this to a secure random string.
dispatchNacId = "141",  -- NAC ID / Password for dispatch channel, change this to your desired ID.

-- Control Keys
controls = {
  talkRadioKey = "B",
  toggleRadioKey = "F6",
  channelUpKey = "",
  channelDownKey = "",
  zoneUpKey = "",
  zoneDownKey = "",
  menuUpKey = "",
  menuDownKey = "",
  menuRightKey = "",
  menuLeftKey = "",
  menuHomeKey = "",
  menuBtn1Key = "",
  menuBtn2Key = "",
  menuBtn3Key = "",
  emergencyBtnKey = "",
  closeRadioKey = "",
  powerBtnKey = "",
},

-- Audio settings
voiceVolume = 60,                 -- Default voice volume (0-100), can be changed in radio settings menu
sfxVolume = 20,                   -- Default sfx volume (0-100), can be changed in radio settings menu
playTransmissionEffects = true,   -- Play background sound effects (sirens, helis, gunshots)
analogTransmissionEffects = true, -- Play analog transmission sound effects (static during transmission)
pttReleaseDelay = 350 -- Delay in milliseconds before releasing PTT to prevent cut-off (250-500ms recommended)
panicTimeout = 60000  -- 1 minute

-- 3D Audio settings (EXPERIMENTAL)
default3DAudio = false, -- true = earbuds OFF by default (3D audio enabled), false = earbuds ON by default (3D audio disabled)
default3DVolume = 0,    -- Default 3D audio volume (0-100), saved per user like voice/sfx volume, default is 50

Security Settings

Change these values for production:

authToken = "your-secure-token-here"    -- Use a strong random string
dispatchNacId = "your-dispatch-password" -- Dispatch panel access code

Available Radio Layouts

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
}

This defines the layouts the radio will use, you can find them in the /client/radios folder.

Auto Layout Assignment

defaultLayouts = {
    ["Handheld"] = "ATX-8000",    -- When on foot
    ["Vehicle"] = "AFX-1500",     -- In ground vehicles
    ["Boat"] = "AFX-1500G",       -- In boats
    ["Air"] = "TXDF-9100",        -- In aircraft
}

These are the default layouts based on the player's state. These can be changed in the radio interface.


Framework Integration

NAC ID System

The permissions system uses NAC (Network Access Code) id's, based on a player's NAC ID, the system determines what zones/channels that user has access to, as well as if they can activate signals or switch to control frequencies.

Implement getUserNacId function:

Standalone Example

getUserNacId = function(serverId)
    -- Return NAC ID based on your logic
    -- Example: static assignment
    if serverId == 1 then
        return "100"  -- Police
    elseif serverId == 2 then
        return "250"  -- EMS
    end
    return "000"  -- Default/Civilian
end

Framework Example

getUserNacId = function(serverId)
    -- Get player data from your framework
    local player = YourFramework.GetPlayer(serverId)
    if player then
        local job = player.job.name  -- Adjust for your framework
        if job == "police" then
            return "100"
        elseif job == "ambulance" then
            return "250"
        end
    end
    return "000"  -- Civilian
end

Player Name Display

getPlayerName = function(serverId)
    local name = GetPlayerName(serverId)
    -- You could extract callsign here if needed
    return name
end

Radio Access Control

radioAccessCheck = function(playerId)
    -- Control who can use the radio system
    -- Return true to allow access, false to deny
    return true  -- Allow everyone by default
end

Signal 100 & Control Frequency Permissions

-- NAC IDs that can activate Signal 100
signalNacIds = {
    "100",  -- Police
    "141",  -- Dispatch
}

Audio Configuration

-- Interference settings
bonkingEnabled = true               -- Enable radio interference
bonkInterval = 750                  -- Interference interval (ms)
interferenceTimeout = 5000          -- How long interference lasts (ms)
blockAudioDuringInterference = true -- Block voice during interference
-- Return true to include background siren in transmission, false otherwise
bgSirenCheck = function()
    local v = GetVehiclePedIsIn(PlayerPedId(), false)
    return v and IsVehicleSirenOn(v) and GetEntitySpeed(v) * 2.237 > 50
end,

Since FiveM does not have a native to check weather only the siren is on, by default we enable the siren if the lights are on and the vehicle is going more than 50mph.

LVC Integration for Siren Check

If you are using Luxart Vehicle Control, you can make the siren check more accurate by placing the following code snippet at the bottom of the UTIL/cl_lvc.lua file in the luxart resource:

exports('sirenCheck', function() -- Returns true if the siren is on
    local siren = false
    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
    if state_lxsiren[vehicle] then
        if state_lxsiren[vehicle] > 0 then
            siren = true
        end
    end
    if state_pwrcall[vehicle] then
        if state_pwrcall[vehicle] > 0 then
            siren = true
        end
    end
    if state_airmanu[vehicle] then
        if state_airmanu[vehicle] == true then
            siren = true
        end
    end
    return siren
end)

After this, you may configure the bgSirenCheck function as such:

bgSirenCheck = function()
    return exports["lvc"].sirenCheck()
end,

Animations Configuration

-- Called when push-to-talk key is pressed or released.
-- Plays animation if enabled. You can return early or empty this function to disable animations.
-- Returns nothing.
onKeyState = function(isKeyDown)
    local ped = PlayerPedId()
    if not ped or ped == 0 then return end

    if isKeyDown then
        -- Start talking: play radio animation
        RequestAnimDict('random@arrests')
        Citizen.Wait(100)
        if HasAnimDictLoaded("random@arrests") then
            if not IsEntityPlayingAnim(ped, "random@arrests", "generic_radio_enter", 3) then
                TaskPlayAnim(ped, "random@arrests", "generic_radio_enter", 8.0, 2.0, -1, 50, 2.0, false, false, false)
            end
        end
    else
        -- Stop talking: stop radio animation
        StopAnimTask(ped, "random@arrests", "generic_radio_enter", -4.0)
    end
end,

-- Called when the radio UI is opened.
-- Loads the animation dictionary so it’s ready when the user talks.
-- Returns nothing.
onRadioOpen = function()
    local ped = PlayerPedId()
    if not ped or ped == 0 then return end

    RequestAnimDict('random@arrests')
end,

Here you can customize how the player animations work for the radio.


Zone and Channel Setup

Basic Zone Configuration

zones = {
    [1] = {
        name = "Statewide",
        nacIds = { "100", "250", "275" },  -- Who can access this zone
        Channels = {
            [1] = {
                name = "DISP", -- Channel name
                type = "conventional", --  Channel type (trunked or conventional)
                frequency = 154.755,                          -- Frequency in MHz
                allowedNacs = { "100" },                      -- Allowed NAC IDs for this channel (can connect and scan)
                scanAllowedNacs = { "110", "200" },           -- NAC IDs that can only scan this channel (cannot connect) 
                gps = {
                    color = 1, -- What color blips should this channel have (Reference: https://docs.fivem.net/docs/game-references/blips/#blip-colors)
                    visibleToNacs = { 100, 250, 275 } -- Who can see this channel's blips
                }
            },
            [2] = {
                name = "TAC-1",
                type = "conventional",
                frequency = 154.755,                          -- Frequency in MHz
                allowedNacs = { "100" },                      -- Allowed NAC IDs for this channel (can connect and scan)
                scanAllowedNacs = { "110", "200" },           -- NAC IDs that can only scan this channel (cannot connect) 
                gps = {
                    color = 2,
                    visibleToNacs = { 100 }
                }
            }
        }
    }
}

Trunked Channel Example

[3] = {
    name = "C2C", -- Channel name
    type = "trunked", -- Channel type
    frequency = 856.1125, -- Control frequency
    frequencyRange = { 856.000, 859.000 }, -- Range of available frequencies
    coverage = 500,  -- Coverage radius in game units
    allowedNacs = { "100" },                      -- Allowed NAC IDs for this channel (can connect and scan)
    scanAllowedNacs = { "110", "200" },           -- NAC IDs that can only scan this channel (cannot connect) 
    gps = { visibleToNacs = { 100 } }
}

GPS Configuration

gps = {
    color = 52,                    -- Blip color (GTA V blip colors)
    visibleToNacs = { 100, 250 }   -- NAC IDs that can see GPS blips
}

Example QBCore Configuration:


Custom Radio Layouts

Creating a Custom Layout

  1. Copy existing layout from client/radios/

  2. Rename folder (e.g., my-custom-radio)

  3. Replace assets:

    • radio.png - Main radio image

    • icons/ folder - Button and indicator icons

    • Custom fonts (optional)

Layout Configuration

Edit config.json in your custom radio folder:

{
  "toneSet": "default",
  "font": "Arial.ttf",
  "fontSize": 12,
  "fontWeight": "normal",
  "radioWidth": 601,
  "radioHeight": 234,
  "radioBGColor": "#e8e8e9",

  "buttons": [
    {
      "key": "power",
      "x": 78, "y": 78,
      "width": 25, "height": 25
    }
  ]
}

Add to Configuration

Add your custom radio to config.lua:

radioLayouts = {
    "AFX-1500",
    "my-custom-radio",  -- Your custom radio
}

Testing

Basic Testing

  1. Start server after configuration changes

  2. Check console for error messages

  3. Test connection with a client

  4. Verify push-to-talk functionality

Debug Mode

Enable verbose logging:

logLevel = 4  -- Enable verbose logging

Backup and Maintenance

Configuration Backup

Backup these files:

  • config.lua - Main configuration

  • Custom radio layouts in client/radios/

Updates

When updating:

  1. Backup current configuration

  2. Clear existing resource folder

  3. Extract new files

  4. Check for any updates to the configuration & restore backup configuration

  5. Test before going live

Last updated