TIMMYG Studios

Setup & Configuration

Complete installation and configuration guide for Tommy's Radio system.

Quick SetupAdvanced ConfigFramework Integration

Quick Start

Prerequisites

  • FiveM Server
  • Firewall configuration access
  • Port available for voice server (default: 7777)

Installation Steps

1. Extract Files

  1. Download the radio resource files
  2. Extract to your server's resources folder
  3. 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:

Basic config.lua 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:

Ubuntu/UFW Example
sudo ufw allow 7777
Windows Example
netsh advfirewall firewall add rule name="Radio Voice Server" dir=in action=allow protocol=TCP localport=7777

Environment 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:

server.cfg
start radio

5. Setup Dispatch Panel

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-secure Chrome 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

radioAccessCheck Function
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
end
getUserNacId Function
Config.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
end

Framework 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

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:

QBCore Integration 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)
end

QBCore 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

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"
end

Zone & Channel Configuration

Basic Zone Structure

Zone Configuration
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

Conventional Channel
{
    name = "DISPATCH",
    type = "conventional",
    frequency = 154.755,
    allowedNacs = { "100" },
    scanAllowedNacs = { "110" },
    gps = { color = 54, visibleToNacs = { "100" } }
}

Trunking Channels

Trunking Channel
{
    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:

Permission Configuration Examples
-- 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
end

Custom Alerts Configuration

Configure custom alerts that can be triggered from dispatch or in-game:

Custom Alerts Configuration
-- 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 dispatchNacId can trigger the first alert from in-game
  • isPersistent alerts stay active until manually cleared
  • toneOnly alerts play sound without visual notification

Custom Tones Configuration

Configure and customize alert tones using frequency-based tone generation:

Default Tones (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 }
  ],
  "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 milliseconds
  • delay - 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:

File Structure Example
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

Audio Settings
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

Interference Configuration
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)

LVC Siren Detection
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.

Siren Detection
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:

Animation Configuration Structure
-- 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 style
  • onKeyState(isKeyDown) - Function called when PTT key is pressed/released
  • onRadioFocus(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

  1. Download the custom LEO poses from GTA5-Mods
  2. Create a folder called radio (or any name) in your rpemotes/stream/ directory
  3. Place all the downloaded animation files (.ycd files) into this folder

Step 2: Register Animations in RPEmotes

Add the following to rpemotes/client/AnimationListCustom.lua inside the CustomDP.Emotes = {} table:

rpemotes/client/AnimationListCustom.lua
["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:

Example: Chest Radio Animation
[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
},
Example: Alternative Handheld Animation
[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" in exports["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 animation
  • onKeyState is triggered when pressing/releasing the PTT key
  • onRadioFocus is triggered when opening/closing the radio UI

Battery System Configuration

Configure how the radio battery system works:

Battery Configuration
-- 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

Radio 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 Context
-- 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

  1. Copy existing layout folder from client/radios/
  2. Rename to your custom name
  3. Replace assets:
    • radio.png - Main radio image
    • icons/ folder - Button icons
    • Update config.json with positioning

Maintenance

Backup These Files

  • config.lua - Main configuration
  • Custom radio layouts in client/radios/

Update Process

  1. Backup current configuration
  2. Extract new resource files
  3. Restore configuration settings
  4. Test before going live

Security Reminder

Always change authToken and dispatchNacId from defaults before production use!