Configuration

Complete config reference for Tommy's Radio — setup flow, zones, channels, codeplugs, access control, audio, and callbacks.

config.luadata.json

Config Sources

SourceContainsEdited via
config/config.luaNetwork, auth, framework, callbacksText editor (requires restart)
data.jsonZones, channels, codeplugs, audio, FX, GPS, emergency, layoutsWeb admin panel (live — no restart)

data.json is managed entirely through the admin panel — changes broadcast to every connected client immediately with no restart needed.


First-Time Setup

After installation, follow this order:

Open the admin panel and create zones and channels

Navigate to http://your-server-ip:port/admin. Go to Zones & Channels and create your zones and channels. See Zones & Channels for field details and channel types.

Create codeplugs

Go to Codeplugs → New. Fill in a name, NAC ID, and allowed models; assign the channel IDs this codeplug can access; save. See Codeplugs for the full creation guide.

Map jobs to codeplugs in sv_functions.lua

Edit config/sv_functions.lua and populate JOB_CODEPLUG_MAP with job name → codeplug ID pairs. See Job → Codeplug Mapping.

Restart the resource

Run restart tRadio in the server console. data.json changes apply instantly; sv_functions.lua and config.lua changes require this restart.


Network & Authentication

config/config.lua
Config = {
    serverAddress = "",           -- Public IP or domain. Leave empty to auto-detect.
                                  -- For reverse proxies: "https://radio.yourdomain.com"
    serverPort    = 7777,         -- Port for voice server and dispatch panel
    authToken     = "CHANGE_ME",  -- Shared secret between game server and voice server. Change before going live.
    adminPassword = "CHANGE_ME",  -- Web admin panel password. Change before going live.
    dispatchNacId = "141",        -- Passphrase dispatchers enter on the dispatch panel login screen
}

Change the defaults before going live

authToken and adminPassword must be changed. Leaving them at "CHANGE_ME" exposes your server to unauthorized admin access.

Three settings that control startup behaviour — checkForUpdates, healthCheck, and logLevel — are managed in Admin Panel → Settings → General tab and stored in data.json. They are not in config.lua.


Framework Integration

config/config.lua
Config = {
    framework = 'auto',  -- 'auto' | 'esx' | 'qbcore' | 'qbx' | 'nd' | 'ox_core' | 'nat2k15' | 'custom'
}

'auto' cascades at startup: qbx → qbcore → esx → nd → ox_core → nat2k15. Falls back to 'custom' if none is detected. Set 'custom' to skip auto-detection and implement all hooks manually via config/sv_functions.lua.

Inventory Item Requirement

config/config.lua
Config = {
    inventory = {
        enabled = false,    -- true = player must carry item to use the radio
        item    = 'radio',  -- item name registered in your inventory resource
        system  = 'auto',   -- 'auto' | 'ox_inventory' | 'qb' | 'esx' | 'custom'
    },
}
ValueInventory checkedNotes
'auto'ox_inventory → framework defaultTries ox_inventory first
'ox_inventory'ox_inventoryWorks standalone or paired with ESX/QB
'qb'qb-inventoryqb-core or qbx_core
'esx'es_extendedMain ESX inventory only
'custom'Your implementationOverride Config.playerHasRadioItem in sv_functions.lua

When enabled = false (default), only a codeplug assignment is required. Grant tRadio.access ACE to bypass the item check for specific players or groups.

Register the item first

The item must be registered in your inventory resource before enabling. tRadio does not auto-register it. For QB, also register a usable item event if you want players to open the radio by using the item from inventory (see Server Callbacks).

For unsupported inventories (VORP, esx_addoninventory), set system = 'custom' and override Config.playerHasRadioItem:

config/sv_functions.lua
-- VORP example:
Config.playerHasRadioItem = function(serverId)
    local item = Config.inventory.item or 'radio'
    local p = promise.new()
    exports.vorp_inventory:getItemCount(serverId, item, function(count)
        p:resolve(count)
    end)
    return (Citizen.Await(p) or 0) >= 1
end

Discord Integration

Discord OAuth for the dispatch and admin panels. Create an application at the Discord Developer Portal and add redirect URIs under OAuth2 → Redirects:

http://your-server-ip:port/radio/dispatch/auth
http://your-server-ip:port/admin/auth/callback
config/config.lua
Config = {
    discord = {
        authEnabled  = false, -- Require Discord login for the dispatch panel
        clientId     = "",    -- Discord app client ID
        clientSecret = "",    -- Discord app client secret
        guildId      = "",    -- Your Discord server ID
        roles        = "",    -- Comma-separated role IDs for dispatch access (empty = any member)
        adminRoles   = "",    -- Comma-separated role IDs for admin panel access (empty = disabled)
        redirectUri  = "",    -- Override OAuth redirect URI (leave empty to auto-detect)
    },
}

Password and Discord auth can be active simultaneously — the login page shows both options.


Zones & Channels

Zones and channels are managed live in Admin Panel → Zones & Channels and stored in data.json. On first run they are seeded from Config.zones in config.lua. After data.json exists, the admin panel values take precedence — edits to Config.zones in config.lua have no effect.

Finding zone and channel IDs

Channel IDs are shown in small text below each channel name in the admin panel — use these in codeplug channelIds. Zone IDs are auto-assigned on creation (format: name-slug-randomchars, e.g. statewide-ark0v) and are not displayed in the admin panel. Leaving zoneIds empty in a codeplug is the most common setup — it grants access to all zones.

Channel Types

Zone and channel configuration in the admin panel

Conventional — single shared frequency. All connected players hear each other regardless of location. Use for most departments and up to ~10 concurrent users per channel.

Trunked — location-based sub-frequency assignment. Units at different locations are placed on separate sub-frequencies automatically, preventing transmission collisions. Dispatchers reach all units via the control frequency. Requires a frequency range (sub-frequency pool) and coverage radius (cell size in meters).

Channel Field Reference

FieldTypeDescription
idstringChannel ID shown below the channel name in the admin panel — use in codeplug channelIds
namestringDisplay name on the radio
typestring"conventional" or "trunked"
frequencynumberFrequency in MHz (must be unique across all zones)
frequencyRangetableTrunked only — { min, max } sub-frequency pool
coveragenumberTrunked only — cell radius in meters
allowedNacstableNAC IDs shown in dispatch/channel list (display filter — does not gate access)
scanAllowedNacstableNAC IDs that appear in the scan list for this channel
modestringAudio codec: "p25_digital" or "analog"
gps.colornumberBlip color ID for GPS markers
gps.visibleToNacstableWhich NAC IDs can see this channel's GPS blips
encryption.txModestring"Clear" or "Secure"
canTransmitboolfalse = dispatch monitor-only channel (listen but not transmit)

Codeplugs

A codeplug is a radio personality template: it defines the player's display NAC ID, zone/channel access, radio model, features (GPS, scan, emergency), and emergency config. Templates are managed live in Admin Panel → Codeplugs — job-to-codeplug mapping is in config/sv_functions.lua.

Codeplug management in the admin panel

Creating a Codeplug

Open Admin Panel → Codeplugs → New

Enter a unique ID (used in JOB_CODEPLUG_MAP and ACE permissions), a Name (display label in the panel), and a NAC ID (cosmetic — shown on the radio screen and dispatch panel, does not gate access).

Set allowed models and default layouts

Under Allowed Models, list the radio model folder names this codeplug may use (e.g. ATX-8000, AFX-1500). Under Default Layouts, pick a default model per context: Handheld, HandheldUnfocused, Vehicle, Boat, Air. Default layouts can also be set server-wide in Admin Panel → Layouts — the codeplug value takes precedence for players assigned this codeplug.

Assign channels

Under Channels, add the channel IDs the codeplug can connect and transmit on. Channel IDs are shown in small text below each channel name in Zones & Channels. Optionally add Scan Channels (listen-only, no transmit). Leave Zone IDs empty to grant access to all zones — only populate it to restrict this codeplug to specific zones.

Enable features

Toggle GPS, Scan, Emergency Button, Man-Down, and Auto-Transmit as needed. At minimum, enable GPS and Emergency Button for most public-safety codeplugs.

Save

Click Save. The codeplug is immediately available for assignment. Players already in-game receive the update on their next radio open.

Minimum required fields: id, name, nac, at least one entry in channelIds.

Codeplug Data Structure

The structure below shows all user-configurable fields as they appear in the admin panel. Internal and computed fields (e.g. server-assigned subscriber IDs) are omitted.

Codeplug field reference (admin panel representation)
{
    id   = "lspd-patrol",
    name = "LSPD Patrol Standard",
    nac  = "141",  -- Display NAC ID on radio/dispatch panel (cosmetic only)

    allowedModels  = { "AFX-1500", "ATX-8000" },
    -- Default layout per context. See Admin Panel → Layouts for server-wide defaults.
    defaultLayouts = {
        Handheld          = "ATX-8000",
        HandheldUnfocused = "ATX-8000",
        Vehicle           = "AFX-1500",
        Boat              = "AFX-1500G",
        Air               = "TXDF-9100",
    },

    -- Channel IDs this codeplug can connect and transmit on.
    -- Channel IDs are shown below each channel name in Admin Panel → Zones & Channels.
    channelIds     = { "sw-disp", "sw-tac1" },
    scanChannelIds = {},   -- listen-only channels (no transmit)

    -- Empty = all zones (most common). Only populate to restrict to specific zones.
    zoneIds = {},

    features = {
        gps             = true,
        scan            = true,
        emergencyButton = true,
        manDown         = false,
        autoTransmit    = true,  -- Auto-transmit on emergency button press
    },

    permissions = {
        supervisor = false,  -- Grants SGN alert button and trunked CT/TK controls
    },

    emergency = {
        emergencyChannelId   = nil,  -- Channel ID to switch to on emergency; nil = stay on current
        autoTransmitDuration = 5,    -- Seconds to auto-transmit (5–120)
    },

    gpsColor      = 54,  -- Blip color for this player's GPS marker
    gpsFlashColor = 1,   -- Blip color during emergency/panic

    scanLists = {},  -- See Scan Lists section below
}

Codeplug Field Reference

FieldTypeRequiredDescription
idstringYesUnique identifier — used in JOB_CODEPLUG_MAP and ACE permissions
namestringYesDisplay name in the admin panel
nacstringYesDisplay NAC ID on radio and dispatch panel — cosmetic only, does not gate access
allowedModelstableNoLayout folder names the player can select (e.g. { "AFX-1500", "ATX-8000" })
defaultLayoutstableNoDefault model per context: Handheld, HandheldUnfocused, Vehicle, Boat, Air. Set server-wide defaults in Admin Panel → Layouts; codeplug values override per-player.
channelIdstableYesChannel IDs the player can connect and transmit on — primary access path. Use IDs from Admin Panel → Zones & Channels.
scanChannelIdstableNoChannel IDs the player can scan (listen only, no transmit)
zoneIdstableNoZone IDs this codeplug can access — empty means all zones
features.gpsboolNoEnable GPS blip visibility
features.scanboolNoEnable the scan feature
features.emergencyButtonboolNoShow emergency button on radio UI
features.manDownboolNoEnable man-down alarm for this codeplug
features.autoTransmitboolNoAuto-transmit when emergency button is pressed
permissions.supervisorboolNoGrants SGN alert button and trunked CT/TK controls in-game
emergency.emergencyChannelIdstring|nilNoChannel ID to switch to on emergency; nil = stay on current
emergency.autoTransmitDurationnumberNoSeconds to auto-transmit on emergency activation (5–120)
gpsColornumberNoBlip color ID for this player's GPS marker
gpsFlashColornumberNoBlip color during emergency/panic
scanListstableNoNamed scan list presets — see Scan Lists

Scan Lists

Named channel groups for the scan feature. features.scan must be true. Configured in Admin Panel → Codeplugs.

Admin panel structure reference
scanLists = {
    {
        id      = "scan-statewide",
        name    = "Statewide",
        entries = {
            { channelId = "ch-dispatch", priority = 1 },  -- 1=High 2=Normal 3=Low
            { channelId = "ch-tac1",     priority = 2 },
        },
    },
}

Unit IDs

Each subscriber receives a unique P25 unit ID derived from their codeplug ID and license hash — consistent across sessions for the same player on the same codeplug.

local info = exports['tRadio']:getSubscriberInfo(serverId)
print(info.unitId)

Job → Codeplug Mapping

Map player jobs to codeplug IDs in config/sv_functions.lua. Keys must match the id field of a codeplug in the admin panel. Changes require restart tRadio.

config/sv_functions.lua
local JOB_CODEPLUG_MAP = {
    ['police']    = 'lspd-patrol',
    ['ambulance'] = 'safd-engine',
    ['fire']      = 'lafd-engine',
}

Config.getPlayerCodeplugId = function(serverId)
    local Player = exports['qb-core']:GetPlayer(serverId)
    if not Player then return nil end
    return JOB_CODEPLUG_MAP[Player.PlayerData.job.name]
end
config/sv_functions.lua
local JOB_CODEPLUG_MAP = {
    ['police']    = 'lspd-patrol',
    ['ambulance'] = 'safd-engine',
    ['fire']      = 'lafd-engine',
}

Config.getPlayerCodeplugId = function(serverId)
    local xPlayer = ESX.GetPlayerFromId(serverId)
    if not xPlayer then return nil end
    return JOB_CODEPLUG_MAP[xPlayer.getJob().name]
end
config/sv_functions.lua
local JOB_CODEPLUG_MAP = {
    ['police']    = 'lspd-patrol',
    ['ambulance'] = 'safd-engine',
    ['fire']      = 'lafd-engine',
}

-- Substitute your framework's job lookup
Config.getPlayerCodeplugId = function(serverId)
    local job = -- your framework job lookup
    return JOB_CODEPLUG_MAP[job]
end

nat2k15: use dept level keys, not job names

nat2k15 exposes player data via exports['framework']:geteverything(). Use p.level keys (e.g., 'lspd_level', 'bcso_level') as keys in JOB_CODEPLUG_MAP instead of job name strings.

As an alternative to JOB_CODEPLUG_MAP, grant tRadio.codeplug.{id} ACE in server.cfg — both paths are equivalent. See ACE Permissions. Players without a matching entry have no radio access.


Access Control

Access Resolution Order

tRadio resolves access in seven steps on every radio open attempt and on every refreshNacId call. A player gets the union of codeplug-based and ACE-based access.

StepGateDeny condition
1Radio accessConfig.getPlayerAccess(serverId)Returns false → no radio at all
2Codeplug requiredJOB_CODEPLUG_MAP, Config.getPlayerCodeplugId, or tRadio.codeplug.{id} ACENo codeplug assigned → no radio
3Zone visibility — codeplug zoneIds or tRadio.zone.{N} ACEEmpty zoneIds = all zones visible
4Channel access — codeplug channelIds, zone membership (step 3), or tRadio.connect.{freq} ACENone of the above → channel hidden
5Scan — codeplug scanChannelIds, connect access (step 4), or tRadio.scan.{freq} ACENone of the above → no scan
6GPS visibility — codeplug visibleCodeplugs or tRadio.gps.{freq} ACENone of the above → no GPS blips
7Admin controls — SGN + trunked CT/TKRequires tlib.admin ACE or permissions.supervisor = true

Call exports['tRadio']:refreshNacId(serverId) after any permission change — access is re-resolved and pushed to the client. Players on a newly inaccessible channel are auto-disconnected. See Refreshing NAC IDs on Job Change for framework-specific event handlers.

ACE Permissions

Radio Access & Codeplug

ACEGrants
tRadio.accessBypass inventory item requirement — player can open the radio without carrying the item
tRadio.codeplug.{id}Directly assign codeplug {id} — equivalent to a JOB_CODEPLUG_MAP entry
server.cfg
add_ace group.admin tRadio.access allow
add_ace identifier.steam:110000100000001 tRadio.codeplug.lspd-patrol allow

Per-Frequency Permissions

Encode the frequency by removing the decimal point: 154.815154815, 856.11258561125.

ACEGrants
tRadio.connect.{freq}Transmit on a frequency
tRadio.scan.{freq}Scan (listen only) on a frequency
tRadio.gps.{freq}See GPS blips for a frequency
tRadio.zone.{N}Access zone N (1-based index)
server.cfg
add_ace group.ems tRadio.connect.154755 allow
add_ace group.ems tRadio.scan.8561125 allow
add_ace group.ems tRadio.gps.154755 allow
add_ace group.ems tRadio.zone.1 allow

Admin Radio Controls

server.cfg
add_ace group.admin tlib.admin allow

tlib.admin grants the in-game SGN alert button and trunked CT/TK toggle. Also grantable per-codeplug via permissions.supervisor = true. Neither grants web admin panel access, which uses adminPassword / Discord auth.

NAC IDs

NAC IDs are display identifiers shown on the radio screen and dispatch panel. They come from the codeplug's nac field and can be overridden at runtime:

exports['tRadio']:setUserNacId(serverId, '142')

NAC IDs do not control zone or channel access. dispatchNacId in config.lua is a separate concept — it is the passphrase dispatchers enter on the dispatch panel login screen.


Server Callbacks

Edit in config/sv_functions.lua. These run server-side on every radio open attempt and on permission refresh.

config/sv_functions.lua
-- Master gate: return false to deny radio access entirely.
-- Codeplug check runs independently — both must pass when inventory is enabled.
Config.getPlayerAccess = function(serverId)
    return true  -- default allows all; codeplug check still runs
end

-- Item check: called when Config.inventory.enabled = true.
-- Override when Config.inventory.system = 'custom'.
Config.playerHasRadioItem = function(serverId)
    -- default: handled automatically for ox_inventory / qb / esx
end

-- Codeplug assignment: return a codeplug ID string, or nil to deny.
-- See Job → Codeplug Mapping for framework-specific examples.
Config.getPlayerCodeplugId = function(serverId)
    return nil  -- replace with: return JOB_CODEPLUG_MAP[getPlayerJob(serverId)]
end

-- Display name shown on the radio and dispatch panel.
Config.getPlayerName = function(serverId)
    return GetPlayerName(serverId)
end

Custom Framework Implementations

For multi-character frameworks or non-standard job structures, set Config.framework = 'custom' and override hooks directly.

config/sv_functions.lua
Config.getPlayerAccess = function(serverId)
    local Player = exports['qb-core']:GetPlayer(serverId)
    if not Player then return false end
    local radioItem = Player.Functions.GetItemByName("radio")
    return radioItem and radioItem.amount > 0 or false
end

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

exports['qb-core']:CreateUseableItem('radio', function(source)
    TriggerClientEvent('radio:use', source)
end)
config/cl_functions.lua
RegisterNetEvent('radio:use', function()
    exports['tRadio']:openRadio()
end)
config/sv_functions.lua
ESX = exports['es_extended']:getSharedObject()

Config.getPlayerAccess = function(serverId)
    local xPlayer = ESX.GetPlayerFromId(serverId)
    if not xPlayer then return false end
    local radioItem = xPlayer.getInventoryItem('radio')
    return radioItem and radioItem.count > 0 or false
end

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
    local callsign = xPlayer.getMeta("callsign")
    if callsign and callsign ~= "" then return callsign end
    local name = xPlayer.getName()
    if name and name ~= "" then
        return string.match(name, "%s+(%S+)$") or name
    end
    return GetPlayerName(serverId)
end

ESX.RegisterUsableItem('radio', function(playerId)
    TriggerClientEvent('radio:use', playerId)
end)
config/cl_functions.lua
RegisterNetEvent('radio:use', function()
    exports['tRadio']:openRadio()
end)

For Qbox, ND_Core, ox_core, and nat2k15, the pattern is identical — substitute the appropriate framework API. See the built-in implementations in config/sv_functions.lua for working starting points.

Refreshing NAC IDs on Job Change

Call exports['tRadio']:refreshNacId(source) when a player's job changes so their channel access updates immediately without a reconnect. This is where the access resolution order (steps 1–7 above) is re-evaluated and pushed to the client.

config/sv_functions.lua
RegisterNetEvent('QBCore:Server:OnJobUpdate', function(source)
    exports['tRadio']:refreshNacId(source)
end)
config/sv_functions.lua
AddEventHandler('esx:setJob', function(source)
    exports['tRadio']:refreshNacId(source)
end)
config/sv_functions.lua
RegisterNetEvent('QBCore:Server:OnJobUpdate', function(source)
    exports['tRadio']:refreshNacId(source)
end)
config/sv_functions.lua
AddEventHandler('ND:SetJob', function(source)
    exports['tRadio']:refreshNacId(source)
end)
config/sv_functions.lua
AddEventHandler('ox_core:setGroup', function(source, groupName, grade)
    exports['tRadio']:refreshNacId(source)
end)
config/sv_functions.lua
-- nat2k15 has no built-in job-change event; refresh on connect instead
AddEventHandler('playerConnecting', function()
    local src = source
    SetTimeout(2000, function() exports['tRadio']:refreshNacId(src) end)
end)

Client Callbacks

Edit in config/cl_functions.lua.

canTalk

Runs before each PTT transmission. Return false to block.

config/cl_functions.lua
Config.canTalk = function()
    if IsPlayerDead(PlayerId()) then return false end
    if IsPedSwimming(PlayerPedId()) then return false end
    return true
end

batteryTick

Controls battery drain and charge rates. Called every frame while the radio is on. deltaTime is in seconds (fractional, ~0.016 at 60 fps) — multiply rates by deltaTime for frame-independent values.

config/cl_functions.lua
Config.batteryTick = function(currentBattery, deltaTime)
    local vehicle = GetVehiclePedIsIn(PlayerPedId(), false)
    if vehicle ~= 0 then
        return math.min(100.0, currentBattery + (0.5 * deltaTime))  -- charge in vehicle
    else
        return math.max(0.0, currentBattery - (0.1 * deltaTime))    -- drain on foot
    end
end

bgModeCheck

Polled every ~500ms. Returns a mode string (or false/nil) that determines which background sound plays behind the voice during a transmission — other players on the channel hear it looping.

ReturnSound played
false / nilNone
"siren"bgSiren.wav
"helicopter"bgHeli.wav
"k9"bgDog.wav
any stringWAV mapped in Admin Panel → Background Sound Modes

The default bgModeCheck cascades through providers: helicopter → K9 → tELS → LVC → ELS-FiveM → JLS → ALS → native GTA siren. Which providers are checked is controlled by Config.sirenSystem.

sirenSystem

config/cl_functions.lua
Config.sirenSystem = 'auto'  -- 'auto' | 'tels' | 'lvc' | 'lvc_fleet' | 'els_fivem' | 'jls' | 'als' | 'native' | 'custom'

Config.sirenResources = {
    tels = 'tELS',
    lvc  = 'lvc',        -- also covers lvc_fleet (shared event system)
    jls  = 'Jace3483_JLS',
    -- els_fivem and als use client events — no entry needed
}

Set 'custom' to skip all provider checks and implement detection directly inside bgModeCheck. Edit sirenResources only if your ELS resource uses a non-standard name.

Playing different WAVs per siren tone (LVC example):

config/cl_functions.lua
if tone == 2 then return "sirenWail" end
if tone == 3 then return "sirenYelp" end
if tone > 0  then return "siren"     end
-- Map "sirenWail" and "sirenYelp" to WAV files in Admin Panel → Background Sound Modes

Forcing an immediate re-poll after a siren state change:

exports['tRadio']:NotifySirenChanged()

Admin Panel Settings

All settings in this section live in data.json and are managed through Admin Panel → Server Settings. Changes apply instantly to all connected clients — no restart needed. The Settings page has four tabs: General, Audio & Radio, Controls, and Advanced.

General Tab

SettingDefaultDescription
checkForUpdatestrueCheck for updates on resource start
healthChecktrueRun port reachability probe at startup
logLevel3Console verbosity: 0=Error 1=Warn 2=Minimal 3=Normal 4=Detailed 5=Verbose
blipsEnabledtrueMaster switch for all GPS blips
gpsBlipUpdateRate50GPS blip update interval (ms). Lower = smoother, higher CPU
signalDegradationEnabledfalseAudio quality degrades with distance from signal towers
signalDegradationIntensity50Max degradation severity at lowest signal (0–100)
signalFalloffRate24Slope of signal drop-off — higher = faster drop
useCallsignSystemtrueEnable the built-in callsign system
callsignCommand"callsign"Chat command to set callsign

Signal tower coordinates are configured in Admin Panel → Settings → Signal Towers — no file editing required.

Callsigns persist client-side in tLib KVP, namespaced by tlib_community_id (falls back to sv_projectName). Two servers sharing the same community ID share callsigns.

Audio & Radio Tab

Volume scales

Admin panel volumes use 0–100. The Lua export API and REST API use 0.0–1.0. Divide by 100 when passing config values to exports (e.g., voiceVolume = 65exports['tRadio']:setVolume(0.65)).

SettingDefaultDescription
voiceVolume65Default voice volume (0–100)
sfxVolume35Default SFX/tone volume (0–100)
volumeStep5Volume adjustment increment per keypress
pttReleaseDelay350Ms to keep transmitting after releasing PTT
pttTriggersProximitytruePTT also triggers proximity voice
playTransmissionEffectstrueEnable background SFX during PTT
analogTransmissionEffectstrueRoute background SFX through the analog FX chain
enable3DAudiofalseMaster switch for 3D audio
default3DAudiofalseInitial per-player default (false = new players join with earbuds on)
default3DVolume50Default 3D audio volume for new players (0–100)
vehicleRadio3DAudioEnabledfalseRadio also plays from the player's vehicle after they walk away
vehicle3DActivationDistance3Meters from vehicle before it takes over as the 3D source

Radio FX

Two independent audio processors. Both can be enabled simultaneously — analog chain is applied on top of vocoder output.

Analog FX chain (fxEnabled) — classic radio sound:

mic → inputGain → highpass → lowpass → compressor → limiter → tube saturation → mid EQ → output

P25 IMBE vocoder (p25Enabled) — real P25 Phase 1 codec. Every listener (in-game, dispatch, 3D bystanders) hears authentic P25 sound. First PTT of a session has a brief cold-start warm-up.

SettingDefaultWhat it does
radioFx.fxEnabledtrueMaster switch for the analog chain
radioFx.p25EnabledfalseMaster switch for the IMBE vocoder
radioFx.inputGain1.65Pre-chain gain
radioFx.highpassFrequency250Rolls off bass below this Hz
radioFx.lowpassFrequency3500Rolls off treble above this Hz
radioFx.compression60Dynamic range compression (0–100)
radioFx.distortion30Tube-saturation drive
radioFx.distortionMode"classic""classic" = hard-clip; "tube" = smooth saturation
radioFx.midBoost2Mid-frequency EQ boost (dB)
ProblemFix
Too muffled / telephone-likeRaise lowpassFrequency (4000–5000)
Too tinny / harshLower highpassFrequency (150–200)
Volume pumpingLower compression
Crackling / distortedLower inputGain (0.8–1.2) or distortion

3D Audio

A player broadcasts in 3D while: enable3DAudio = true, their Earbuds toggle is off, and they are transmitting, receiving, or playing a tone. Range: 100 m (person) or 200 m (vehicle source). With vehicleRadio3DAudioEnabled on, entering a vehicle claims it as a 3D source; walking more than vehicle3DActivationDistance m away hands the source role to the vehicle.

Controls Tab

SettingDefaultDescription
bonking.blockTransmissiontrueSuppress the second PTT when someone is already transmitting (enforced server-side)
bonking.playBonkTonetruePlay a bonk tone to the blocked user
bonking.bonkToneGlobaltrueAlso play the bonk tone to everyone on the channel
bonking.doubleTapOverridetrueDouble-tapping PTT within doubleTapWindow bypasses the block
bonking.doubleTapWindow1500Double-tap window (ms)

Default keybinds are also configured on this tab and stored in data.json. Players can rebind any key in FiveM → Settings → Key Bindings → FiveM.

Advanced Tab

SettingDefaultDescription
panicTimeout60000Ms before panic auto-clears
defaultManDownEnabledfalseEnable man-down alarm for new players by default
manDownWarningDelay10000Ms before warning tone plays after player goes incapacitated
manDownEmergencyDelay30000Ms after warning before full emergency alarm triggers
defaultTtsEnabledtrueEnable voice announcements (TTS) for new players by default
animationsEnabledtrueAllow PTT/focus animations
focusLayoutModefalseDefault state of the on-foot focus/unfocus split layout toggle

Earbud Clothing Detection

Automatically enables earbuds mode when a player wears specific clothing items. Configured in the admin panel — no restart required.

FieldTypeDescription
slotIdnumberGTA V component/prop slot ID
slotTypestring"prop" for accessories, "component" for clothing
drawableIdnumberThe drawable index to match

Checked every 7 seconds. Manual earbud toggles are respected until clothing state changes again.

On this page

Need help?

Ask on Discord