Skip to content

Configuration

Complete configuration reference for Tommy's Radio — config.lua, framework hooks, zones and channels, codeplugs, every admin panel setting, sounds, and integrations.

Every setting tRadio has, and which of the five places it lives in. Where it lives decides whether it needs a restart.

Where each setting lives

SourceContainsEdited inApplies
config/config.luaNetwork, auth, Discord, framework, inventoryText editorAfter restart tRadio
config/sv_functions.luaServer hooks — access, player names, 911 caller ID, AI toolsText editorAfter restart tRadio
config/cl_functions.luaClient hooks — canTalk, battery, background sound, animationsText editorAfter restart tRadio
data.jsonZones, channels, codeplugs, alerts, speakers, towers, geo zones, all server settingsAdmin panelInstantly
layouts/<Model>/How each radio model's screen is drawn — see Radio layoutsText editorAfter restart tRadio

Check the admin panel first. Open it at /tradio → Admin Panel, or at <your tLib URL>/dispatch/admin.

Set these four, then stop

  1. tlib_http_public_url in server.cfg — only if tLib's auto-detection gets your address wrong.
  2. authToken — 20+ random characters. Only federation reads it.
  3. adminPassword and dispatchNacId, or Discord OAuth. 141 is the demo server's code.
  4. Custom framework only: the hooks in config/sv_functions.lua and config/cl_functions.lua.

config.lua

Server and authentication

lua
Config = {
    exposeWeb = true,
    showDispatchInHub = true,
    showAdminInHub = true,
    useLegacySocketTransport = false,
    hostServerUrl = "",            -- satellites only
    isSatelliteServer = false,
    authToken = "",                -- federation only
    adminPassword = "",            -- empty = generated on first boot
    dispatchNacId = "141",
}
KeyDefaultDescription
exposeWebtruePublish the browser panels through tLib. Not needed in game. Required on a federation host
showDispatchInHubtrueList Dispatch on tLib's hub. Visibility only. Restart required
showAdminInHubtrueList Radio Admin as a sibling destination. Hidden is not private. Restart required
useLegacySocketTransportfalseDeprecated since 7.0. Game-client Socket.IO fallback. Needs exposeWeb = true. Leave it false
hostServerUrl""Satellite-only URL of the federation host, including its tLib mount path
isSatelliteServerfalseShare a host's radio backend from another game server
authToken""Federation shared secret, 20+ random characters. Empty means the endpoint answers 401
adminPassword""Web admin password. Empty generates one on first boot into data.json. A value set here wins
dispatchNacId"141"Browser dispatch login passphrase, not channel access. Change it

Public web addressing belongs to tLib: tlib_http_public_url and tlib_http_port. The URL is optional and takes a bare IP, an ip:port, or a domain.

Discord authentication

Optional OAuth for both panels. Create an application at the Discord Developer Portal, then add both redirect URIs under OAuth2 → Redirects:

http://203.0.113.10:7777/dispatch/radio/dispatch/auth
http://203.0.113.10:7777/dispatch/admin/auth/callback

The Panel row of the console banner is the exact base. Discord matches redirect URIs exactly, so a missing /dispatch mount segment fails with invalid_redirect_uri.

lua
Config = {
    discord = {
        authEnabled  = true,
        clientId     = "1284739201847362048",
        clientSecret = "<your discord client secret>",
        guildId      = "938271046582910384",
        roles        = "938271046582910111,938271046582910222",
        adminRoles   = "938271046582910333",
        redirectUri  = "",
    },
}
KeyDefaultDescription
authEnabledfalseRequire Discord login for the dispatch panel in a browser. In-game login is separate
clientId""Developer Portal → General Information
clientSecret""Developer Portal → OAuth2
guildId""Easiest in Admin Panel → Credentials, which lists your servers
roles""Comma-separated role IDs. "" = any guild member
adminRoles"""" = no Discord admin access
botToken""Set from Credentials; never shown back to you
redirectUri""Override the OAuth origin. "" derives it from tLib's public URL. Only needed behind a reverse proxy

Store a bot token in Credentials and the page turns every Discord role into a checkbox. Sessions last 24 hours; role changes apply at next login.

When you need a bot token. The browser login never does. The in-game login does; without one, in-game access rests on the tradio.dispatch / tradio.admin ACE permissions alone.

redirectUri must be an origin — https://radio.example.com, not radio.example.com. A path is ignored, and a bad value falls back to tLib's public URL.

Giving out radios by Discord role

Credentials → Role → codeplug. It stacks with the jobs / ace / item routes; a player admitted by both keeps the higher job grade.

Roles are read when a player first needs a codeplug, then every five minutes.

Most Discord-role resources already write ACE principals, so ace = 'tradio.leo' may suffice.

In-game panel access

Neither panel asks for a password or a NAC ID in game. The check runs before the panel opens: a player without access sees one chat line and no panel.

  1. ACE permission — tradio.dispatch and tradio.admin:

    cfg
    add_ace group.admin tradio.admin allow
    add_ace group.mod   tradio.dispatch allow
  2. Discord roles — only if a bot token is stored and discord.guildId is set. Your bot must be in the guild with the Server Members Intent enabled.

Framework detection

lua
Config = {
    framework          = 'auto',
    frameworkResources = {
        esx = 'es_extended', qbcore = 'qb-core', qbx = 'qbx_core',
        nd = 'ND_Core', ox_core = 'ox_core', nat2k15 = 'framework',
    },
    autoRefreshOnJobChange = true,
}
KeyDefaultDescription
framework'auto''auto', 'esx', 'qbcore', 'qbx', 'nd', 'ox_core', 'nat2k15' or 'custom'
frameworkResourcessee aboveResource names used for detection. Change only if you renamed one
autoRefreshOnJobChangetrueListen for your framework's job-change event instead of the ~15 s poll

'auto' probes qbx → qbcore → esx → nd → ox_core → nat2k15, falling back to 'custom'. nat2k15 always polls. 'custom' means you write the server hooks.

A framework you set yourself is never overridden.

Who gets a radio

A codeplug is the radio's programming, and holding one is access. No codeplug, no radio.

tRadio ships open. Out of the box every player is granted the leo codeplug, so the radio works the moment the resource starts, with or without a framework:

lua
Config = {
    codeplugGrants = {
        leo = { everyone = true },
    },
}

Lock it down when your framework is up by replacing that line with the routes you want:

lua
Config = {
    codeplugGrants = {
        leo  = { jobs = { police = 0, sheriff = 0, ambulance = 0, fire = 0 } },
        fire = { jobs = { fire = 0 } },
    },
}

Keys are codeplug IDs from Admin Panel → Codeplugs. Each entry lists ways to be granted it, and any one is enough:

KindExampleMeans
jobs{ police = 0, sheriff = 2 }Job name → minimum grade
ace'tradio.leo'An ACE grant
item'leo_radio'Carrying an item
customfunction(src) return … endYour own check, returning a boolean
everyonetrueNo restriction

tradio.codeplug.<id> is always accepted as an ACE, listed here or not.

A player holds exactly one codeplug. When several match, the one demanding the highest job grade wins; a tie goes to codeplug ID, and the console names both.

Requiring a physical radio item

lua
Config = {
    requireItem = 'radio',   -- nil requires no item
}

An item required in addition to a codeplug, for everybody. It is checked first, so a player missing both is denied for the item. To exempt someone, grant their codeplug through ace, custom or everyone.

Inventory item requirement

lua
Config = {
    inventory = { system = 'auto' },
}
KeyDefaultDescription
system'auto''auto', 'ox_inventory', 'qb', 'esx' or 'custom'
enabled—Retired — use Config.requireItem. Read only when requireItem is absent
item—Retired — use Config.requireItem. Read only when requireItem is absent

With Config.requireItem set, tRadio makes that item usable on ox_inventory, QBCore, QBX and ESX by itself. 'auto' detects what your inventory resource supports, including forks.

Carrying the item and using it are two things. A failed registration costs only opening-by-use, but a failed item check denies every player. See Every player is denied radio access.

Opening the radio by using the item

Route 1 — items.lua. Recommended, and universal. No exports, no framework, no minimum version.

lua
-- ox_inventory/data/items.lua
['radio'] = {
    label = 'Radio',
    weight = 500,
    client = { event = 'tradio:client:openRadio' },
},

Route 2 — automatic registration. tRadio probes your inventory at startup and registers every method it can prove will fire:

MethodUsed when
ox_inventory usingItem hookox_inventory reports 2.43.0 or newer
ox_inventory:usedItem eventThe hook could not be proven — forks, older ox, bridges
QBCore / QBX CreateUseableItemThose frameworks, including alongside ox_inventory
ESX RegisterUsableItemESX, including alongside ox_inventory

When nothing can be proven, tRadio prints the items.lua line to add plus a Detected: summary. Quote it in a ticket.

Custom and forked inventories

Two hooks in config/sv_functions.lua, both shipping with working defaults.

For the item check, set system = 'custom' and replace Config.playerHasRadioItem:

lua
Config.playerHasRadioItem = function(serverId)
    local item = Config.requireItem 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

For opening the radio by use, fill in Config.registerRadioItemUse, a stub returning false:

lua
Config.registerRadioItemUse = function(itemName, openEvent)
    exports['my_inventory']:RegisterUsableItem(itemName, function(source)
        TriggerClientEvent(openEvent, source)
    end)
    return true
end
  • Called once at startup, in a pcall, only when system = 'custom' or auto-detection proved nothing
  • Receives itemName and openEvent; fire that event at the player
  • Return true if you registered something; anything else falls through to the items.lua advice

Scanner

A scanner is receive-only — no PTT, and the server refuses one. It is an ordinary codeplug with scanner = true, granted in codeplugGrants.

lua
codeplugGrants = {
    scanner = { item = 'police_scanner' },
}

A Scanner codeplug ships as a default alongside LEO, carrying the three default zones and twelve channels with SWX-250 as its layout. It is granted to nobody until you map it.

A scanner with an empty channel list renders as a radio that does nothing — the usual reason a working scanner looks broken. Without a grant for a secure channel it shows ENC.

AI dispatcher

Config.aiDispatch is the only part of the dispatcher in a file. Whether it is on, its channels, callsign and voice are in Admin Panel → Settings → AI Dispatcher; what it may do is Config.aiDispatchTools.

lua
Config.aiDispatch = {
    providers   = {},                          -- all three local
    localEngine = { root = "", textModel = "", transcribeModel = "", voicesDir = "" },
    voices      = { male = { "en_US-ryan-medium" }, female = { "en_US-amy-medium" } },
    wakePhrases = { "dispatch" },
    demeanor    = "",                          -- measured
}
KeyDefaultDescription
providers{}Per-capability hosted provider for text, speech and transcription. See Where inference runs
localEngineall ""Override engine and model locations. Empty means the copies in this resource
voicesPiper voicesOnly the first of each is used. What a voice is depends on who serves speech
wakePhrases{ "dispatch" }Traffic not starting with one of these is unit-to-unit, and is left alone
demeanor""measured, brisk, veteran or formal. Blank is measured. See How it carries itself

The rest is on AI dispatcher.

Vehicle radio prop

Where it sits is positioned in game per vehicle (/tradio → 3D Prop Editor in the sidebar, needs tlib.admin). This section is which model spawns.

lua
Config = {
    vehicleProp = {
        default = 'ztr540',
        models  = {
            { model = 'ztr540', label = 'ZTR-540 (built in)' },
        },
    },
}
KeyDefaultDescription
default'ztr540'Model for a placement that names none. Anything not in models falls back to ztr540
modelssee aboveThe list offered in the prop editor. model is the archetype name; label is cosmetic

tRadio streams exactly one model itself, ztr540, which cannot be removed. The models list is read at startup.

model is the archetype name from the .ytyp, not the file name, and is case-insensitive. Get it wrong and the vehicle falls back to default.

Behind a reverse proxy

The panels are served on tLib's port, so that is what a proxy sits in front of. The service speaks plain HTTP, and a proxy on 127.0.0.1 needs no trusted-proxy entry.

  • Forward WebSocket upgrades — HTTP-only forwarding serves the panel, then fails every connection
  • Point the proxy at tlib_http_port, never tRadio's internal loopback port
  • Raise the read and send timeouts — nginx cuts a quiet WebSocket at 60 seconds
nginx
location / {
    proxy_pass http://127.0.0.1:7777;   # must equal tlib_http_port
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    # Without these, nginx closes an idle WebSocket after 60s.
    proxy_read_timeout 86400s;
    proxy_send_timeout 86400s;
}

List your proxy. An unlisted one makes every player count as the same client for rate limiting.

txt
set tradio_trusted_proxies "172.17.0.1, 10.0.0.5"

Required whenever the proxy is not on localhost. Loopback is always trusted, an IPv4 entry also matches its IPv6-mapped form, and the value is read once at startup.


Framework integration

Assigning codeplugs to jobs

The mapping lives under codeplugGrants. sv_functions.lua now only answers what job does this player hold.

The lookup is case-insensitive on all six frameworks. On nat2k15, dept level keys ('lspd_level') and display names ('LSPD') both work. Every job the player holds counts, including ox_core groups and qb gangs.

Different codeplugs per rank. Give each its own minimum grade; the highest the player clears wins. Below, grades 0–2 get leo, 3–7 leo-supervisor and 8+ leo-command.

lua
codeplugGrants = {
    leo                = { jobs = { police = 0 } },
    ['leo-supervisor'] = { jobs = { police = 3 } },
    ['leo-command']    = { jobs = { police = 8 } },
    fire               = { jobs = { fire = 0 } },
},

With no framework running, jobs grants nobody. Use ace, item, custom or everyone on a standalone server.

Server hooks

All in config/sv_functions.lua, shipping with working implementations for every supported framework. Edit them only to change behavior.

HookReturnsCalled
Config.getPlayerAccess(serverId)booleanEvery server-side radio event. false denies the radio entirely
Config.playerHasRadioItem(serverId, item)booleanWhenever an item is asked about. Replace when system = 'custom'
Config.getPlayerCodeplugId(serverId)codeplug ID or nilAccess resolution. Shipped version resolves Config.codeplugGrants
Config.getPlayerName(serverId)stringThe name on radios and the dispatch panel

getPlayerName's 'custom' branch falls back to GetPlayerName(serverId) — the Steam or FiveM display name, not an RP character name. Return a string, never nil; serverId ≤ 0 means the dispatcher.

lua
Config.getPlayerName = function(serverId)
    if not serverId or serverId <= 0 then return "DISPATCH" end
    local p = MyFramework.GetPlayer(serverId)
    if p and p.charinfo then
        return p.charinfo.firstname .. ' ' .. p.charinfo.lastname
    end
    return "Player " .. serverId
end

Refreshing access after a job change is automatic on QBCore, QBX, ESX, ND_Core and ox_core. To trigger it yourself, call exports['tRadio']:refreshNacId(source).

Client hooks

In config/cl_functions.lua.

Config.canTalk() — runs while PTT is held; return false to block transmission. Returning false mid-transmission force-stops PTT and logs canTalk check failed mid-transmission to F8 — the most common non-hardware cause of "my mic doesn't work".

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

Config.batteryTick(currentBattery, deltaTime) — returns the new battery level, deltaTime in seconds. As shipped, any vehicle charges. Battery Drain in Settings → General skips this hook entirely.

lua
Config.batteryTick = function(currentBattery, deltaTime)
    local vehicle = GetVehiclePedIsIn(PlayerPedId(), false)
    if vehicle ~= 0 then
        return math.min(100.0, currentBattery + (0.5 * deltaTime)) -- full charge in ~3 min
    end
    return math.max(0.0, currentBattery - (0.1 * deltaTime))       -- full drain in ~17 min
end
HookWhat it does
Config.bgModeCheck()Polled every 500 ms. Returns a mode string for the background sound behind your voice, or false. Ships with "helicopter", "k9" and the siren provider. See Background sounds
Config.k9ModelsPed models that trigger "k9". Seven GTA dog models ship
Config.animationsThe animation list players choose from. See Animations
Config.screenRangeMetres within which another vehicle's dash radio is drawn. Default 8.0; your own is exempt, 0 leaves only that

Zones and channels

Managed live in Admin Panel → Zones & Channels. A zone is a group of channels; a channel is one frequency inside it.

A new zone is invisible until a codeplug references it

Players get channels from their codeplug, so a zone in no codeplug is invisible — no error, no warning. This is the most common "I set it up and nothing works" report. Two steps:

  1. Zones & Channels → Add Zone, then add channels and save
  2. Codeplugs → Channel Access, on every codeplug that should hear them

An unreferenced zone carries a Not in any codeplug badge, and saving a new one prompts once.

Channel types

Conventional — one shared frequency. Everyone connected hears everyone else regardless of location. Use this for almost everything.

Trunked — units get a sub-frequency by location, so crews across the map do not step on each other. Needs a frequency range and a coverage radius. Dispatchers work it two-way through its control frequency, and the log groups the talkgroup under one entry.

Direct / simplex channels

A conventional channel can use the Direct / Simplex RF path: radio-to-radio, no repeater, no towers. RF path is separate from Mode, so a direct channel can be analog or P25.

SettingDefaultRangeWhat it does
Direct range150050–10000 mDistance at which reception ends. Bars fall 5 to 1 across it
Obstructed range351–100 %Share of range left when terrain sits between two nearby radios
Dispatch base radiooff—Fixed map position and routing bucket for dispatch
  • Reception is per radio. Two units 3 km apart can both talk without hearing each other. A two-bar lead wins; similar strength gets neither
  • Channel busy is per radio too — only traffic your own radio hears blocks you
  • Signal bars show what you are receiving, not coverage
  • Speakers, vehicle radios, gunshots, explosions and sirens follow the same rule
  • Players in different routing buckets never hear each other
  • Dispatch needs a base radio, or the channel is left off the panel entirely

Obstruction checks are short-range — tested under 500 m, and can only shorten the range. Longer links use the distance envelope alone.

Direct channels cannot be trunked, patched or grouped — each would act as a hidden repeater. Channels sharing a frequency must share RF settings, or that frequency carries no direct traffic.

Channel fields

FieldTypeDefaultDescription
Namestring—Display name on the radio, e.g. DISP
Frequencynumber—MHz. Must be unique across all zones
Typeselectconventionalconventional or trunked
RF pathselectrepeaterrepeater or direct (stored as rfMode)
Modeselectanalogp25_digital runs the channel through a vocoder
System typeselectimbeDigital only (stored as codec): imbe is P25 Phase 1, c2-3200 is M17's Codec 2
DispatchtoggleoffMonitor only — dispatchers listen, never transmit (canTransmit: false)
Frequency rangetwo numbers—Trunked only. Start and end of the sub-frequency pool
Coveragenumber—Trunked only. Cell radius in metres
Security intentselectclearMarks the channel encrypted and lights the secure (⊘) icon. Does not scramble audio
GPS color overrideblip IDunsetOverrides the codeplug's GPS color for players on this channel

What secure does. The icon shows steadily and blinks on an encrypted call, and an ungranted scanner shows ENC. It does not scramble audio.

Each channel has a generated channel ID shown under its name (disp-lx1gi). That ID is what codeplugs reference — not the frequency, not the name.

Trunked channels need a valid frequency range and coverage. Range start must be below range end and coverage above 0. Sub-frequencies step by 0.0125 MHz, so a narrower range hands out nothing.

A panel save is refused with the reason. A hand-edited file is coerced where it safely can be, so check the console after a hand-edit.

Frequencies, coverage and frequencyRange normalize to four decimal places. Startup also reports, by name, a missing Channels list, a missing or non-numeric frequency, a missing type, and two channels sharing a frequency inside one zone.

Reordering zones and channels

Each zone card and channel row has a drag handle (⠿), and the new order is what players see. Channels reorder only within their zone.

Hiding a zone from dispatch

A Hide from dispatch toggle keeps a zone's channels off the panel and its traffic out of the log and user counts. In game it works as before.

Announcement groups

A group is a named set of channel IDs: a dispatcher arms one and their next PTT fans out to all of them. Each can get its own PTT key. Two ship: * DISP and * C2C.

Geo zones

Admin Panel → Coverage → Areas. A circular area that switches a player's radio to a target frequency when they walk in, and back when they leave.

FieldDescription
Position (X, Y, Z)World coordinates of the centre
RadiusMetres
Vertical boundsOptional min/max world Z for stacked areas. Min must be ≤ max
Target frequencyMust match a configured channel, or the save is rejected

Auto-switching is opt-in per player and per zone: the master Geo Auto-Switch toggle plus the zone's checkbox in /tradio → Behavior → Zones. /tradio_debug coords copies your position.


Codeplugs

A codeplug is a radio personality: which channels a player gets, which models they can pick, which features are on, and what the emergency button does. Managed in Admin Panel → Codeplugs; granted in codeplugGrants.

Codeplug sections

SectionWhat you set
IdentityID (locked after creation — grants reference it), name, and NAC. The NAC is cosmetic and gates nothing
Allowed ModelsWhich radio models players may select. Selecting none leaves them with no radio
Default LayoutsModel shown first per context: on-foot focused and unfocused, ground, water, air. Per-spawncode overrides win
Channel AccessChannels this codeplug can connect and transmit on. Scan access follows automatically. Zone access is derived
Scan AccessExtra listen-only channels
Scan ListsNamed lists, priorities, talkback and hang time, and which list each channel scans. See Scan lists
Home SoftkeysSoftkey banks for conventional and trunked channels, plus the menu timeout. See Home softkey banks
DisplayClock format, date order, top display zone letter. See Display
FeaturesGPS Blips, Scan, Emergency Button, Man Down. GPS Blips off disallows GPS rather than defaulting it off — the server withholds the position
Supervisor AccessThe in-game Alrt key and the trunked Ctrl/Trnk keys, without server admin rights
Emergency ConfigurationButton hold, keep-alive, profiles, per-channel assignment, Man Down timers. See Emergency profiles
GPS VisibilityWhich other codeplugs' blips this one sees, plus its own blip and panic flash colors
Scannerscanner = true makes this codeplug receive-only. See Scanner

Minimum viable codeplug: an ID, a name, one allowed model, one channel. Changes reach a player the next time their job loads or you call refreshNacId.

Scan lists

A radio scans the list of the channel it is on, the way a real one does. Players turn scanning on and off and edit lists on the radio; see Scanning.

PriorityMeaning
Priority 1Takes over from any other scanned traffic; cannot be nuisance-deleted
Priority 2Takes over from non-priority channels; cannot be nuisance-deleted
Non-priorityHeard when nothing higher is busy. New entries start here

A real radio has one Priority 1 and one Priority 2 per list, and the panel warns when a list has more. Giving a channel a priority takes it from the one that had it.

Which list each channel scans, under Scan Lists → Which list each channel scans:

SettingChoices
Default for this codeplugPersonal (built on the radio), No scan list, or one of the codeplug's lists
Per channelUse default, No scan list, or a specific list

With lists but no default, unassigned channels scan the first list. With none at all, every player gets a Personal list built with Edit Scan. Scan shows No scan list or List empty accordingly. Deleting a list returns its channels to the default.

Talkback and hang time. Each list has both, and a row above the lists sets them for the Personal list and anything on Use default:

SettingChoicesMeaning
Transmit while scanningUse default, Selected channel, TalkbackSelected channel (the default): PTT uses the selected channel. Talkback: PTT answers on the scanned channel the radio is stopped or parked on
Hang time0.5–10 s (default 1)How long the radio stays parked on a scanned channel after it goes quiet

The panel warns when a Talkback list holds scan-only channels, where a stopped player gets the bonk. Satellite players cannot use talkback at all.

Home softkey banks

Codeplugs → Home Softkeys programs the Home softkey row. Two banks: conventional (and no channel) and trunked. Leave a bank on Use default, or Customize to choose and order the keys.

KeyAction
Zone / ChanSelect Zone / Select Chan
ScanScan on/off; hold for Edit Scan
ScnLEdit Scan
NuisNuisance delete
LocLocation sharing
InfoInfo → Radio Info
OptnOptions
ZnUp / ZnDnChange zone immediately
ChUp / ChDnChange channel immediately
VolOpens Voice vol
ClckRead-only Clock screen
AlrtSupervisors: fires the first configured alert on the channel
Ctrl / TrnkSupervisors, trunked: control frequency / sub-frequency

Defaults are ZnUp Zone ZnDn Chan Scan Nuis ScnL Optn Loc Info then Alrt (conventional) or Ctrl/Trnk (trunked). Clck, ChUp/ChDn and Vol appear only if added. That order leaves Chan on page two of a three-key radio.

Unusable keys are skipped and the row closes up: scan keys without scan, Loc without GPS, supervisor keys without Supervisor Access. The panel warns, without blocking, when a bank omits Zone, Chan or Optn.

Menu timeout. Off by default; Turn on sets 30 seconds, and 5–300 is allowed. An idle radio returns to Home with the MENU_INACTIVE_EXIT tone, discarding anything unconfirmed. See Menu timeout.

Display

SettingChoices
Clock12-hour (01:45pm, default) or 24-hour (13:45)
Date orderMonth/Day/Year (default) or Day/Month/Year. A date shows only when Radio clock time is the player's computer
Top display zone letterNone (default), Basic bank (A–F for zones 1–6) or Enhanced bank (A–Y, one letter per three zones, zones 1–75)

The time comes from Settings → General → Radio clock time. The zone letter appears only on the QRX-720H top view.

Emergency profiles

Codeplugs → Emergency Configuration programs the emergency button: every channel runs one profile. See Emergency for how each mode behaves.

SettingMeaning
Button holdHold time for the orange button, 50–6200 ms (default 1000). Ending is always a one-second hold
Keep radio on during emergencyThe radio cannot be powered off until the emergency clears
ProfilesOne or more named profiles; a codeplug always keeps at least one
Man Down timersPre-alert and post-alert seconds. Blank uses the server's Warning Delay / Emergency Delay

Each profile has:

FieldChoices
ModeAlarm — ends on acknowledgment or exhausted retries. Call — a priority call, trunked only. Alarm + Call — alarm, then call on acknowledgment, exhausted retries, or PTT
SilentAlarm and Alarm + Call only. The sending radio shows and plays nothing; dispatch still sees it
ChannelTactical (stay put) or Revert to a channel this codeplug can use. A reverted radio returns when the emergency ends
Hot mic0–120 s of self-keyed microphone on the first emergency transmission. 0 = off. Auto Transmit refuses it
Alarm retry every / RetriesRepeat interval while unacknowledged, and retries before No acknowledge. Not used by Call

Which profile each channel uses works like scan list assignment: a default plus per-channel overrides. A No emergency channel gives a short reject tone, a conventional channel inheriting Call runs it as Alarm + Call, and Emergency Button off disables emergency everywhere.

A codeplug saved before profiles existed shows as a single Default profile built from its original settings.

Server-wide safety limits sit outside the codeplug: Panic Alert Timeout and Panic Alert Cooldown in Settings → Audio & Radio.

How access is resolved

Every radio open attempt and every refreshNacId call runs this chain. A player gets the union of codeplug access and ACE grants.

StepGateDenied when
1Config.getPlayerAccess(serverId)Returns false — no radio at all
2Codeplug assignment via codeplugGrants, getPlayerCodeplugId, or tradio.codeplug.{id}No codeplug — no radio
3Zone visibility — derived from selected channels, or tradio.zone.{N}No channel in the zone and no ACE
4Channel connect — codeplug channel access or tradio.connect.{freq}Neither — channel hidden
5Scan — codeplug scan access, connect access, or tradio.scan.{freq}Neither — no scan
6GPS visibility — codeplug GPS Visibility list or tradio.gps.{freq}Neither — no blips
6aOn the map — the codeplug's GPS Blips feature, then the player's GPS switchEither off — they see blips but are not one
7Supervisor controls (Alrt, trunked Ctrl/Trnk)Requires tlib.admin or the codeplug's Supervisor toggle

Nothing configured? With no framework, no Discord mapping and only jobs routes, tRadio runs standalone: every job-mapped codeplug goes to every player, and the console prints one Standalone: line. Add any other route, and grants are read strictly.

ACE permissions

An alternative or supplement to codeplug assignment. tradio.codeplug.{id} has the highest priority and overrides codeplugGrants.

ACEGrants
tradio.codeplug.{id}Assign codeplug {id} directly
tradio.connect.{freq}Connect and transmit on a frequency
tradio.scan.{freq}Scan (listen only) a frequency
tradio.gps.{freq}See GPS blips on a frequency
tradio.zone.{N}Access zone N by its 1-based position in the zone list
tlib.adminIn-game Alrt, trunked Ctrl/Trnk, and the 3D prop editor. Not the web admin panel
txt
add_ace identifier.steam:110000100000001 tradio.codeplug.leo allow
add_ace group.ems tradio.connect.154755 allow
add_ace group.ems tradio.scan.8561125 allow
add_ace group.ems tradio.zone.1 allow
add_ace group.admin tlib.admin allow

Frequency keys drop the decimal point: 154.815 → 154815. A precise form keeps it as an underscore — 154_815 — because dropping it is lossy: 1.23 and 12.3 both become 123. tRadio warns on startup if yours collide.

Pre-7.0 tRadio.* spellings are still honoured, and the console names each once per start.


Server settings

Admin Panel → Server Settings, four tabs. Every value applies live to all connected clients unless marked restart required.

General tab

SettingDefaultDescription
Check for UpdatestrueCheck for new tRadio versions on start
Include Pre-release UpdatesfalseAlso pull release candidates
911 CallingfalseMaster switch, off until you turn it on. Enforced immediately server-side, but /911 only disappears for players who reconnect. Restart required
911 Call Cooldown30 sMinimum wait between a player's calls. 0 disables it; a 5-second backstop still applies
Low Battery ChirptrueWarning chirp under 50% battery, more frequent under 30%
Battery DraintrueOff holds every battery at 100% and skips Config.batteryTick
Log Level30 Error · 1 Warnings · 2 Minimal · 3 Normal · 4 Debug · 5 Verbose
Radio BlipstrueMaster switch for minimap blips of players on your channel
Blip Update Rate50 msClient-side render interval. Below 50 ms is not recommended
Radio AnimationstrueShow the animation selector in player settings
Focus Layout ModefalseA separate skin when the UI is open but unfocused. Needs a HandheldUnfocused layout
Enable CallsignstrueLet players set a personal callsign
Callsign CommandcallsignChat command name, without the slash. Restart required
Man-Down by DefaultfalseNew players have man-down monitoring on
Warning Delay45 sMotionless time before the Man Down post-alert. Range 10–300. A codeplug's timers override it
Emergency Delay20 sPost-alert time before the emergency is raised. Range 5–120. A codeplug's timers override it
TTS by DefaulttrueNew players hear spoken zone and channel announcements
Radio clock timeGame worldGame world: the in-game clock, no date. Player's computer: their local time and date. Format is per codeplug under Display

The three Man-Down settings are the only ones that raise an emergency without a key press. Read Man down first.

Audio & Radio tab

Audio Defaults — applied when a player first connects.

SettingDefaultDescription
Voice Volume65Default radio voice volume (0–100)
Tone Volume35Default tone and effect volume (0–100)
3D Audio Volume50Default 3D spatial volume (0–100)
Volume Step5Change per press of a volume keybind. Range 1–25

PTT & Transmission

SettingDefaultDescription
PTT Release Delay350 msHow long the radio stays open after releasing PTT. Range 0–2000
Transmit Time-Out60000 msThe radio's own TOT: a key held this long is cut off, with a warning four seconds earlier. 0 turns it off; otherwise 5000–1800000, and less is raised to 5 s
PTT Triggers ProximitytrueWith no radio equipped, PTT activates proximity voice. Requires MumbleVoice
Transmission TonestruePlay TX_START / TX_END when someone begins or ends transmitting
Analog TonestrueUse analog squelch sounds (PTT / PTT_END, heard by the transmitter) instead of digital receiver tones
Panic Alert Timeout60 sHow long an emergency stays active before auto-clearing, for crashed or AFK units. 0 = manual only
Panic Alert Cooldown15 sMinimum wait before the same player raises another emergency there. Never blocks clearing one

Half-Duplex (Bonking) — what happens when someone keys up over another unit.

SettingDefaultDescription
Block TransmissiontrueDeny the PTT attempt. Off, it goes through but a collision tone plays
Play Bonk TonetruePlay a tone to the blocked user
Bonk Tone to AlltrueEveryone on the channel hears it
Double-Tap OverridetrueTwo quick PTT presses force a blocked transmission through
Double-Tap Window1500 msHow close the presses must be. Range 100–5000

3D Positional Audio — everything below the master switch is hidden until it is on.

SettingDefaultDescription
Enable 3D AudiofalseMaster switch
3D Audio On by DefaultfalseSets each new player's Earbuds default, inverted. On: Earbuds off, their radio is audible to others. Off: Earbuds on, their radio emits nothing. Never overrides a choice a player has made
Vehicle 3D AudiotrueA vehicle the player has been in keeps rebroadcasting their radio
Vehicle 3D Distance3 mHow far the player must walk before the vehicle takes over as the source
Player Speaker Range35 mHow far a handheld carries
Vehicle Speaker Range60 mHow far a vehicle-sourced radio carries

Signal Coverage

SettingDefaultDescription
Always Full SignalfalseForce full bars and no degradation, without deleting your towers
Signal DegradationfalseReduce audio quality with distance from the nearest tower
Falloff Rate24How fast quality drops. 0 gradual, 100 steep
Degradation Intensity50How severe degradation is at low signal

Degradation does nothing until towers exist — see Signal towers.

Dispatch Scanning — Simultaneous Scan Audio (default false). Off, a transmission on the dispatcher's main channel mutes scanned channels. On, every scanned channel plays independently.

Radio Audio Effects — see Radio FX.

Keybinds

The radio key shows a hidden radio, focuses a visible one, and hides a focused one. Escape returns control to the game. Rebind in FiveM → Settings → Key Bindings → FiveM.

Key bindingDefault
Radio - Push to TalkB
Radio - Open / Hide RadioF6
Radio - Voice Volume Up / Down= / -
Radio - Tone Volume Up / Down] / [
Radio - Power On/Offunbound
Radio - Hide Radiounbound
Radio - Emergency (hold)unbound — hold for the codeplug's button hold time
Radio - Light / Dimmerunbound — follows the radio's Light or Dimmer
Radio - Menu Arrow Up / Down / Left / Rightunbound
Radio - Home Buttonunbound
Radio - Softkey 1 / 2 / 3unbound
Radio - Channel Up / Down, Zone Up / Downunbound
Radio - Next / Previous Radio Modelunbound — hidden unless Radio Animations is on
Radio - 3D Volume Up / Downunbound — hidden unless 3D Audio is on

Advanced tab

Earpiece / Items — GTA outfit slots that switch earbuds on automatically. Clothing is checked every 7 seconds; a manual toggle holds until it changes.

FieldDescription
Typeprop for accessories, component for clothing
SlotThe GTA slot — props include Hat/Helmet (0), Glasses (1), Ear/Earpiece (2), Watch (6), Bracelet (7)
Drawable IDThe 0-based clothing variant index

Networking & Limits — Control Message Rate Limit (default 120, range 30–2000). Control-plane messages per connection per 10 seconds — joins, leaves, scan edits, PTT starts. Voice is never counted.

LB Phone / LB Radio — see LB Phone and LB Radio. CAD / MDT — see CAD / MDT.


Radio layouts

Each radio model is a folder in layouts/, its screen described by ui.html using data- attributes. None of it changes channels, menus or keys. Apply with restart tRadio.

ui.html attributeWhat it does
data-radio-default-scaleStarting size 0.5–1.5 for a player with no saved size. DRX-330 1.25, QRX-720 1.15, QRX-720H 0.8
data-radio-power-source"vehicle" hides the battery icon. Set on HLX-120, HLX-120G, ZTR-540, ZTR-540S and KVD-880
data-fit-min (on a text element)Smallest size text may shrink to. Longer text is shortened with …
data-radio-persistent-message="tint"Top-view style: an emergency or Man Down tints the glass instead of a message strip. QRX-720H uses it

Everything else is ordinary CSS in the layout's ui.html. Copy a shipped model's folder and edit it.


Sounds

Admin Panel → Sounds. All changes broadcast instantly.

Tone catalog

Every tone is a named slot mapped to a WAV. Map them under Global Tone Mappings, and upload new WAVs from Marketplace → Sounds.

TonePlays when
PTT / PTT_TRUNKED / PTT_PATCHEDYou key up — conventional / trunked / patched
PTT_END / PTT_END_TRUNKED / PTT_END_PATCHEDYou release
TX_START / TX_START_TRUNKED / TX_START_PATCHEDSomeone else keys up
TX_END / TX_END_TRUNKED / TX_END_PATCHEDSomeone else releases
BEEPGeneric alert beep
BONKTransmission collision
CHIRPShort acknowledgment, also the low-battery chirp
ECHOMessage received
PANICA unit's emergency starts, and each unacknowledged alarm retry. Not for Silent
EMERGENCY_ENTRY / EMERGENCY_EXITYour own emergency starts / ends
EMERGENCY_REJECTThe emergency button on a channel with no emergency, or power-off blocked by keep-alive
EMERGENCY_NO_ACKThe alarm ran out of retries without an acknowledgment
EMERGENCY_NO_EMERGENCYRepeats while an emergency is paused on a channel with no emergency
CONSOLE_ACKFour tones: a dispatcher acknowledged your emergency
MAN_DOWN_CONTINUOUSRepeating during the Man Down post-alert
MENU_INACTIVE_EXITThe menu timeout returned the radio to Home
TOT_WARN / TOT_EXPIREDTransmit time-out warning / time-out
ALERT_A / ALERT_B / ALERT_CDefault alert-tone slots
ALERT_SIGNAL3Signal 3 alert tone
PRIORITY / PRIORITY_REPEATPriority alert activation and each repeat
RING_OUT / RING_INHeard by a 911 caller while ringing / by dispatchers when a call arrives
GUNSHOTA player fires while transmitting (2D and 3D)
EXPLOSIONA nearby explosion is picked up while transmitting (2D, 3D and dispatch)

Per-model overrides replace one tone for one model, or drop WAVs at layouts/{MODEL}/sounds/{TONE_NAME}.wav.

GUNSHOT ships as bgShot.wav, the one slot not named after its file. Clearing the mapping restores it rather than silencing gunshots.

EXPLOSION defaults to explosion.wav. Networked explosions within 150 m of a transmitting player are picked up; client-local ones are not.

Adding a sound

Drop the file into layouts/sounds/ — no folder to create, no manifest to write. The server lists any .wav there on the Sounds page, without a restart.

text
layouts/sounds/
  ptt.wav
  transStart.wav
  airhorn.wav              <- yours

.ogg and .mp3 play too and install from the marketplace, but a bare .ogg dropped in by hand is not auto-discovered.

Background sounds

These fixed-filename WAVs in layouts/sounds/ play during a received transmission and cannot be renamed:

FilePurpose
transStart.wavClick-in at the start of a received transmission
transMid.wavLooping background noise during it
transEnd.wav / transEnd1.wav / transEnd2.wavClick-out — one of the three at random
bgSiren.wavSiren loop
bgHeli.wavHelicopter rotors
bgDog.wavK9

bgShot.wav is not here: it is the stock file behind the remappable GUNSHOT slot.

Background Sound Modes maps what Config.bgModeCheck returns to WAV files. Three ship: siren, helicopter, k9; unmapped modes play nothing. For a WAV per siren tone, return distinct modes:

lua
if tone == 2 then return "sirenWail" end
if tone == 3 then return "sirenYelp" end
if tone > 0  then return "siren"     end
ProviderWhere the tone index is
tELSbag['tels:sirenTone'] on Entity(vehicle).state
LVCstate_lxsiren — 1 Airhorn, 2 Wail, 3 Yelp, 4 Priority, 5+ custom
LVC FleetSet per vehicle in the VCF config files
ELS-FiveMnewstate — 1–3 are audible

Siren detection

Which ELS resource bgModeCheck reads to decide a siren is on.

ProviderValue
Auto — try each installed provider in orderauto (default)
tELS onlytels
Luxart Vehicle Control onlylvc
LVC Fleet onlylvc_fleet
ELS-FiveM onlyels_fivem
JLS onlyjls
Advanced Lighting System onlyals
Native GTA siren onlynative
Skip detection — implement bgModeCheck yourselfcustom

Auto order is tELS → LVC / LVC Fleet → ELS-FiveM → JLS → ALS → native. Each returns "not my vehicle" for a car it does not know, so mixed fleets work. Force a re-poll with exports['tRadio']:NotifySirenChanged().

Radio FX

The DSP chain applied to incoming radio voice: mic → input gain → highpass → lowpass → compressor → limiter → saturation → mid EQ → output.

SettingDefaultRange
Radio FXtrueMaster switch
Distortion Modeclassicclassic hard-clip, tube smooth saturation
Highpass Cutoff250 Hz80–800
Lowpass Cutoff3500 Hz1200–8000
Compression600–100
Distortion300–100
Mid Boost2 dB−12 to 12
Input Gain1.650.5–3.0
ProblemFix
Too muffled, telephone-likeRaise Lowpass Cutoff to 4000–5000
Too tinny or harshLower Highpass Cutoff to 150–200
Volume pumpingLower Compression
Crackling or clippedLower Input Gain to 0.8–1.2, or lower Distortion

Digital is separate and per-channel — there is no global P25 switch. Set Mode to P25 Digital and System type to IMBE or M17 codec2. The analog chain applies on top, and the session's first transmission has a brief warm-up.

Update server and clients together. A client from an earlier release decodes the new IMBE frames with the old engine and hears distorted, overloud audio.

About Codec 2. The M17 system type is Codec 2, copyright David Rowe and contributors, under the LGPL 2.1. It is a separate, replaceable module, and nothing else in tRadio is covered by that license.


Alerts

Admin Panel → Alerts. An alert banners every radio on the channel, and the first alert in the list is what the in-game Alrt key fires. Each has three phases with optional tones: activate, repeat (persistent only), deactivate.

FieldDefaultDescription
Name—Display text on the radio
Color—Hex color of the banner
PersistentfalseStays active until manually cleared
Tone—Single tone used for all phases
Multi-toneoffSwitches to separate activate / repeat / deactivate tones
Repeat everyrandom 5–10 sGap between repeats. Minimum 1000 ms
Show banner on repeattrueFlash the banner on each repeat
Deactivate labelRESUMEBanner text shown on clearance
Deactivate color#126300Color of the clearance banner
Tone OnlyfalsePlay the tone with no visual banner
Channel filterall channelsRestrict this alert to specific channels

Four ship by default: SIGNAL 100, SIGNAL 3, Ping and Bonk.


Speakers

Admin Panel → Coverage → Speakers. World-placed tone-board speakers a dispatcher can broadcast to. Grab coordinates with /tradio_debug coords; dispatchers use the Speakers button (see Working the board).

FieldDefaultDescription
Position (X, Y, Z)—World coordinates
Heading0Prop rotation in degrees. Only affects the visible prop
Radius60 mAudible reach, and who receives the tone. Applies from the next tone
Prop ModelnoneOptional. Blank places no prop; the speaker still plays from its coordinates
GroupnoneSpeakers sharing a group name are one selectable target on the panel

Signal towers

Admin Panel → Coverage → Towers. Signal strength falls off with distance from the nearest tower, so coverage is a union of reaches, not a sum. Leave gaps where players should lose signal.

FieldDescription
NameLabel in the panel
Position (X, Y, Z)World coordinates
Prop modelOptional. Blank places no prop; the tower still provides coverage

Integrations

LB Phone and LB Radio

Fully managed in Settings → Advanced. Nothing to configure in config.lua, and the two are independent.

SettingDefaultDescription
LB Phone 911 Dial-InOffPlayers dial 911 from the in-game phone and get call status as notifications. Requires lb-phone
LB Radio Frequency MirrorOffMirrors frequency, connection status and user count into LB Phone's Radio app. Read-only. Needs lb-radioapp or lb-radio started

The dial-in carries a restart badge: each player gets one 911 entry point on connect, and FiveM cannot un-register a chat command mid-session. The mirror applies live.

CAD / MDT

Fully managed in Settings → Advanced, and both toggles apply live.

SettingDefaultDescription
Mirror 911 Calls Into Your CADtrueEvery accepted 911 call becomes a CAD record with the caller's location
Mirror Panic Into Your CADtrueRaise an emergency in your CAD when a unit hits their panic button

There is nothing to install or key in. LACORE, SonoranCAD, ImperialCAD, wasabi_mdt, tk_mdt and ps-mdt are detected from the running resource. What each supports is listed under AI Dispatcher → CAD.

Panic is a call. Every CAD with a panic concept implements it as a call carrying an urgency marker. Those with a dedicated officer-in-distress alert get that instead.

ImperialCAD needs its resource started under exactly that name, with its own imperial_community_id and imperialAPI convars. A mirrored call is deleted only if it dies unanswered.

911 hooks

Two optional hooks in config/sv_functions.lua, both with sensible defaults.

lua
-- The caller ID a dispatcher sees. A real 911 dispatcher only gets a number, so this —
-- not getPlayerName — is what the call card shows. Default: the caller's real LB Phone
-- number, otherwise a stable 555-XXXX placeholder derived from their license.
Config.getCall911CallerNumber = function(serverId)
    return "555-0142"
end

-- Fires when a call is placed while no dispatch panel is connected. The call still queues.
-- Default: chat-broadcast to every player with radio access.
-- `call` fields: callerName, callerUnitId, callerNumber, message, coords, frequency, createdAt.
Config.call911NoDispatcherFallback = function(call)
    -- Discord webhook, NPC dispatch, or your own alerting here
end

Multi-server federation

Run several game servers against one shared voice server and dispatch panel.

  • Host (Server A) runs the voice backend and must set exposeWeb = true
  • Satellites run no backend. They federate player, panic, tone and alert state to the host, needing outbound access only
lua
-- host
Config = {
    exposeWeb         = true,
    isSatelliteServer = false,
    hostServerUrl     = "",                          -- must stay empty on the host
    authToken         = "<20+ random characters>",
}

-- each satellite
Config = {
    hostServerUrl     = "http://203.0.113.10:7777",  -- the host's public URL, port included
    isSatelliteServer = true,
    authToken         = "<same value as the host>",
}

A satellite with an empty hostServerUrl is rejected at startup with isSatelliteServer=true requires hostServerUrl to be set to the host server's URL.

Satellites pull config every 30 seconds, and 911 calls need the host's voice server.

Next steps

Still stuck? Support can answer it directly.

Was this page helpful?