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

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 Integration

QBCore Example
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 "100"
        elseif job == "ambulance" then return "200"
        end
    end
    return "000"
end

Config.getPlayerName = function(serverId)
    local player = exports['qb-core']:GetPlayer(serverId)
    if player and player.PlayerData.metadata.callsign then
        return player.PlayerData.metadata.callsign
    end
    return GetPlayerName(serverId)
end

ESX Integration

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

Permission Configuration
-- Radio Access Control
Config.radioAccessCheck = function(playerId)
    -- Control who can use radios
    return true  -- Allow everyone by default
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 Sound Detection

Siren Detection
Config.bgSirenCheck = function()
    local playerPed = PlayerPedId()
    local vehicle = GetVehiclePedIsIn(playerPed, false)

    if vehicle == 0 then return false end

    -- Check if siren is on and vehicle is moving
    local speed = GetEntitySpeed(vehicle) * 2.237 -- Convert to mph
    return IsVehicleSirenOn(vehicle) and speed > 50
end

LVC Integration

If using Luxart Vehicle Control, add this export to LVC's cl_lvc.lua:

LVC Export (Add to LVC)
exports('sirenCheck', function()
    local playerPed = PlayerPedId()
    local vehicle = GetVehiclePedIsIn(playerPed, false)
    if not vehicle or vehicle == 0 then return false end

    -- Check LVC siren states
    local siren = false
    if state_lxsiren[vehicle] and state_lxsiren[vehicle] > 0 then siren = true end
    if state_pwrcall[vehicle] and state_pwrcall[vehicle] > 0 then siren = true end
    if state_airmanu[vehicle] == true then siren = true end

    return siren
end)

Then update your radio config:

Use LVC Integration
Config.bgSirenCheck = function()
    return exports["lvc"]:sirenCheck()
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,
        onRadioOpen = function()
            -- 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
  • onRadioOpen() - Function called when radio UI is opened

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

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!