# 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!

{% hint style="info" %}
fx\_version 'bodacious'
{% endhint %}

### 2. Start the Resource

Add to `server.cfg`:

```
start radio
```

{% hint style="info" %}
It is important to keep the resource name to "radio" for export usage.
{% endhint %}

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

```bash
# Ubuntu/UFW
sudo ufw allow 7777

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

{% hint style="info" %}
This process may vary based on your server's operating system, yet should be fairly easy to perform.
{% endhint %}

### 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](https://tommys-scripts.gitbook.io/fivem/paid-scripts/tommys-radio/..#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](https://medium.com/@om_bhandari/how-to-use-chrome-flags-unsafely-treat-insecure-origin-as-secure-for-local-development-0c0591b92f46) for your dispatch panel site, and everything should work as normal.

#### SSL/HTTPS Setup (Advanced / Recommended)

**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`

{% hint style="info" %}
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.
{% endhint %}

***

## Basic Configuration

### config.lua

<pre class="language-lua"><code class="lang-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 &#x26; 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 = {
<strong>  talkRadioKey = "B",
</strong>  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
</code></pre>

### Security Settings

Change these values for production:

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

### Available Radio Layouts

```lua
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
}
```

{% hint style="info" %}
This defines the layouts the radio will use, you can find them in the `/client/radios`  folder.
{% endhint %}

### Auto Layout Assignment

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

{% hint style="info" %}
These are the default layouts based on the player's state. These can be changed in the radio interface.
{% endhint %}

***

## Framework Integration

### NAC ID System

{% hint style="info" %}
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.
{% endhint %}

Implement `getUserNacId` function:

#### Standalone Example

```lua
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

```lua
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

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

### Radio Access Control

```lua
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

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

### Audio Configuration

```lua
-- 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
```

```lua
-- 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,
```

{% hint style="info" %}
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.
{% endhint %}

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

```lua
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:

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

### Animations Configuration

```lua
-- 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,

```

{% hint style="info" %}
Here you can customize how the player animations work for the radio.
{% endhint %}

***

## Zone and Channel Setup

### Basic Zone Configuration

```lua
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

```lua
[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

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

***

### Example QBCore Configuration:

{% file src="<https://4246691052-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeMH8Dlb2xv2Hk8JLeePe%2Fuploads%2FW4ZKkx9IZJ93nLexgFLN%2Fconfig%20(1).lua?alt=media&token=55ca4237-55fd-4404-bc1c-b176b3d8ded4>" %}

***

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

```json
{
  "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:

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

```lua
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
