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.


Understanding Port Requirements

Your server needs THREE separate ports:

  1. FiveM Server Port (usually 30120) - Players connect to your game server
  2. txAdmin Port (usually 40120) - For server management panel
  3. 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:

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

  1. Go to https://portchecker.co/check-v0
  2. Enter your server IP and the radio port (e.g., 192.0.2.100:50978)
  3. 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:

  1. Try restarting your entire server from the panel (not just the resource)
  2. Check if the resource started successfully - Look for errors in console
  3. 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 7777

CentOS/RHEL (firewalld):

sudo firewall-cmd --permanent --add-port=7777/tcp
sudo firewall-cmd --reload

Windows Server:

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

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

  1. Forward port 7777 (TCP) in your router settings
  2. Configure your router's port forwarding rules (similar to Minecraft server setup)
  3. 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:

server.cfg
start radio

5. Setup Dispatch Panel

Access your panel at http://yourserverip:yourport/ after the resource starts.

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:

  1. Open app and login with 141 (demo default)
  2. Click settings cog (⚙️) at top-right
  3. Change Endpoint URL to: http://192.0.2.100:7777/ (use your actual IP and port)
  4. Save - app will refresh
  5. Login with YOUR dispatchNacId from 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:

  1. In config.lua, enable Discord auth:
config.lua
useDiscordAuth = false, -- Set to true to enable Discord authentication
  1. Create a .env file in the server folder of the resource:
server/.env
# 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=""
  1. Get your Discord credentials from the Discord Developer Portal
  2. 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

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:

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

Add 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 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)
end

ESX 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

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 Examples

QBCore - Require Radio Item
Config.radioAccessCheck = function(playerId)
    local Player = exports['qb-core']:GetCoreObject().Functions.GetPlayer(playerId)
    return Player and Player.Functions.GetItemByName("radio") ~= nil or false
end
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

config.lua
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 dispatchNacId can trigger first alert
  • isPersistent keeps alert until manually cleared
  • toneOnly plays sound without visual notification

Custom Tones Configuration

Edit client/radios/default/tones.json:

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 milliseconds
  • delay - 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

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

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).

config.json Element Examples
{
  "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_A

Custom Tone Format:

client/radios/default/tones.json
{
  "TONE_NAME": [
    { "freq": 900, "duration": 40, "delay": 60 },
    { "freq": 1200, "duration": 50 }
  ]
}
  • freq: Frequency in Hz (100-4000)
  • duration: Length in milliseconds
  • delay: 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

  1. Check config version - Open your current config.lua and note the version at the top:
    -- Config Version 3.3
  2. Backup current configuration
  3. Extract new resource files
  4. 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
  5. Restore custom settings (authToken, dispatchNacId, zones, channels, etc.)
  6. 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!

On this page