Files
Elite-Gaming-FiveM/resources/radio/config.lua
T
2026-03-13 16:26:33 -07:00

699 lines
34 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- Tommy's Radio System Configuration
-- Documentation: https://docs.timmygstudios.com/docs/tommys-radio
-- Config Version 4.0 - Use a website like https://www.diffchecker.com/ to compare configuration file changes
Config = {
-- ┌──────────────────────────────────────────────────────────────┐
-- │ RADIO LAYOUTS │
-- └──────────────────────────────────────────────────────────────┘
-- All available radio layout models. These appear in the radio style
-- settings menu and can be assigned as defaults below.
radioLayouts = {
"AFX-1500",
"AFX-1500G",
"ARX-4000X",
"XPR-6500",
"XPR-6500S",
"ATX-8000",
"ATX-8000G",
"ATX-NOVA",
"TXDF-9100",
},
-- Default layout per vehicle type or spawn code. Vehicle types are
-- "Handheld", "Vehicle", "Boat", and "Air". You can also add entries
-- keyed by a vehicle's spawn code (e.g., "police") to override the
-- vehicle-type default for specific models.
defaultLayouts = {
-- Vehicle type defaults
["Handheld"] = "ATX-8000",
["Vehicle"] = "AFX-1500",
["Boat"] = "AFX-1500G",
["Air"] = "TXDF-9100",
-- Per-spawn-code overrides (add as many as you need)
["fbi2"] = "XPR-6500",
["police"] = "XPR-6500",
},
-- ┌──────────────────────────────────────────────────────────────┐
-- │ KEYBINDS │
-- └──────────────────────────────────────────────────────────────┘
-- Default key mappings for radio controls. Leave a value as "" for
-- no default bind — players can still bind them in FiveM settings.
-- Key names use FiveM key identifiers (e.g., "B", "F6", "LSHIFT").
controls = {
-- Primary controls
talkRadioKey = "B", -- Push-to-talk
toggleRadioKey = "F6", -- Open / close the radio UI
closeRadioKey = "", -- Alternative close key
powerBtnKey = "", -- Toggle radio power on/off
-- Channel & zone navigation
channelUpKey = "",
channelDownKey = "",
zoneUpKey = "",
zoneDownKey = "",
-- Menu navigation
menuUpKey = "",
menuDownKey = "",
menuRightKey = "",
menuLeftKey = "",
menuHomeKey = "",
menuBtn1Key = "",
menuBtn2Key = "",
menuBtn3Key = "",
-- Misc
emergencyBtnKey = "", -- Trigger emergency / panic button
-- Radio style cycling
styleUpKey = "",
styleDownKey = "",
-- Volume hotkeys
voiceVolumeUpKey = "",
voiceVolumeDownKey = "",
sfxVolumeUpKey = "",
sfxVolumeDownKey = "",
volume3DUpKey = "",
volume3DDownKey = "",
},
-- ┌──────────────────────────────────────────────────────────────┐
-- │ NETWORK & AUTHENTICATION │
-- └──────────────────────────────────────────────────────────────┘
-- Final connection string sent to clients. Include protocol and/or port
-- if using a proxy (e.g., "https://proxy.example.com"). When empty, the
-- server's auto-detected IP address and serverPort are used instead.
serverAddress = " ",
-- Port for the radio voice server and dispatch panel. Choose a port not
-- used by other resources on your server.
serverPort = 7777,
-- Secure token for radio authentication. Change this to a long, random
-- string — it protects the voice server and dispatch panel API.
authToken = "changeme",
-- NAC ID / password for the dispatch channel. In-game players whose NAC
-- ID matches this value can access the trunked control frequency and
-- trigger SGN alerts from their radio.
dispatchNacId = "EGRP-LAW2026",
-- Enable Discord-based authentication for the dispatch panel. Requires
-- a Discord application configured in server/.env (see server/.env.example).
useDiscordAuth = false,
-- ┌──────────────────────────────────────────────────────────────┐
-- │ CALLSIGN SYSTEM │
-- └──────────────────────────────────────────────────────────────┘
-- When enabled, players can set their own callsign via an in-game
-- command, and dispatchers can set callsigns from the dispatch panel.
-- Callsigns are stored in client KVP storage and persist across
-- sessions and server restarts.
--
-- When disabled, the getPlayerName function (see Server Callbacks
-- below) is the only source for player display names.
--
-- When enabled, getPlayerName acts as the default/fallback for
-- players who haven't set a custom callsign yet.
useCallsignSystem = true,
callsignCommand = "callsign", -- In-game command (e.g., /callsign 2L-319). Set to "" to disable.
-- ┌──────────────────────────────────────────────────────────────┐
-- │ GENERAL │
-- └──────────────────────────────────────────────────────────────┘
checkForUpdates = true, -- Check for script updates on resource start
logLevel = 3, -- 0 = Error, 1 = Warnings, 2 = Minimal, 3 = Normal, 4 = Debug, 5 = Verbose
pttReleaseDelay = 350, -- Milliseconds before releasing PTT to prevent audio cut-off (250-500 recommended)
panicTimeout = 60000, -- Milliseconds before a panic alert auto-clears
-- When true, pressing PTT on the radio will also trigger proximity
-- voice chat so nearby players can hear you speaking in-game.
pttTriggersProximity = true,
-- ┌──────────────────────────────────────────────────────────────┐
-- │ AUDIO │
-- └──────────────────────────────────────────────────────────────┘
voiceVolume = 65, -- Default voice volume (0-100), adjustable in radio settings
sfxVolume = 35, -- Default SFX volume (0-100), adjustable in radio settings
volumeStep = 5, -- Increment per press of volume up/down hotkeys (1-20 recommended)
playTransmissionEffects = true, -- Play background sound effects during transmissions (sirens, helis, gunshots)
analogTransmissionEffects = true, -- Play analog static sound effects during transmissions
-- ┌──────────────────────────────────────────────────────────────┐
-- │ RADIO AUDIO FX │
-- └──────────────────────────────────────────────────────────────┘
-- Two independent systems control how received voice sounds.
-- They can be enabled in any combination:
--
-- fxEnabled only → clean voice shaped by the web-audio filter chain
-- (bandpass, compression, saturation). Sounds like a
-- decent analog radio with no digital codec character.
--
-- p25Enabled only → true IMBE vocoder (encode → WASM decode every frame).
-- Sounds exactly like a real P25 digital radio.
-- Voice quality is independent of the FX parameters.
--
-- both enabled → vocoder runs first, then the FX chain shapes the
-- output further (adds analog warmth on top of the
-- digital codec character).
--
-- neither enabled → completely clean, unprocessed voice audio.
radioFx = {
-- ── Web Audio FX chain ──────────────────────────────────────
-- Bandpass filter, dynamics compression, and tube saturation
-- applied to all received voice. Works well on its own as a
-- lightweight "radio sound" without any vocoder overhead.
fxEnabled = false, -- Master switch for the FX chain below.
highpassFrequency = 250, -- Hz (80800) Low cut. Removes rumble/low-end.
-- 250 Hz is a clean radio sound without
-- eating the low harmonics of deep voices.
lowpassFrequency = 3400, -- Hz (12008000) High cut. 3400 Hz = telephone band.
distortion = 20, -- (0100) Tube saturation / analog warmth.
compression = 60, -- (0100) Dynamic range crushing.
midBoost = 2, -- dB (-12 to 12) Presence boost at ~1200 Hz.
inputGain = 1.2, -- (0.53.0) Pre-amp before the chain.
-- ── P25 IMBE Vocoder ────────────────────────────────────────
-- True IMBE encode→decode loop. Produces the
-- characteristic P25 digital radio sound. More CPU than fxEnabled
-- alone but bit-accurate to real P25 Phase 1 hardware.
p25Enabled = true, -- Master switch for the IMBE vocoder.
},
-- 3D Audio (EXPERIMENTAL) — when enabled, radio audio is spatially
-- positioned in the game world instead of playing in 2D.
enable3DAudio = false, -- Master switch for the 3D audio system
default3DAudio = false, -- true = earbuds OFF by default (3D enabled), false = earbuds ON (3D disabled)
default3DVolume = 50, -- Default 3D volume (0-100), saved per user
vehicle3DActivationDistance = 3.0, -- Min distance (meters) from vehicle before its 3D audio activates
-- ┌──────────────────────────────────────────────────────────────┐
-- │ GPS & SIGNAL │
-- └──────────────────────────────────────────────────────────────┘
-- How often GPS blips are updated (milliseconds). Lower = smoother but
-- higher CPU usage. Increase if you experience performance issues.
-- Recommended: 50 (smooth), 100 (balanced), 250 (performance), 500 (low-end)
gpsBlipUpdateRate = 50,
-- Signal tower positions used for the signal-strength icon on the radio.
-- NOTE: These do NOT affect voice quality — they are cosmetic only.
signalTowerCoordinates = {
{ x = 1860.0, y = 3677.0, z = 33.0 },
{ x = 449.0, y = -992.0, z = 30.0 },
{ x = -979.0, y = -2632.0, z = 23.0 },
{ x = -2364.0, y = 3229.0, z = 45.0 },
{ x = -449.0, y = 6025.0, z = 35.0 },
{ x = 1529.0, y = 820.0, z = 79.0 },
{ x = -573.0, y = -146.0, z = 38.0 },
{ x = -3123.0, y = 1334.0, z = 25.0 },
{ x = 5266.79, y = -5427.7, z = 139.7 },
},
-- ┌──────────────────────────────────────────────────────────────┐
-- │ BATTERY │
-- └──────────────────────────────────────────────────────────────┘
-- Called every second to update battery level. Return the new level (0-100).
-- @param currentBattery number Current battery level (0-100)
-- @param deltaTime number Seconds since last update
-- @return number New battery level (0-100)
batteryTick = function(currentBattery, deltaTime)
local playerPed = PlayerPedId()
local vehicle = GetVehiclePedIsIn(playerPed, false)
if vehicle ~= 0 then
-- Charge battery when in a vehicle
local chargeRate = 0.5 -- % per second
return math.min(100.0, currentBattery + (chargeRate * deltaTime))
else
-- Discharge battery when on foot
local dischargeRate = 0.1 -- % per second
return math.max(0.0, currentBattery - (dischargeRate * deltaTime))
end
end,
-- ┌──────────────────────────────────────────────────────────────┐
-- │ BONKING / CHANNEL COLLISION │
-- └──────────────────────────────────────────────────────────────┘
-- Controls what happens when you try to key up while someone else
-- is already transmitting on your connected channel.
--
-- blockTransmission
-- true → You cannot talk over someone. Your PTT is denied.
-- false → You can talk over someone (both transmit simultaneously).
--
-- playBonkTone
-- true → When a PTT attempt is denied (or allowed-but-colliding,
-- depending on blockTransmission), a bonk tone is broadcast
-- to the channel so everyone hears the collision indicator.
-- false → Collisions are silent; no tone is played.
--
-- doubleTapOverride
-- Only meaningful when blockTransmission = true.
-- true → If your PTT was just denied (bonked), pressing PTT again
-- within doubleTapWindow ms bypasses the block and lets you
-- transmit anyway (override). This lets operators force
-- through urgent traffic.
-- false → Every PTT attempt while the channel is busy is denied,
-- no override possible.
--
-- doubleTapWindow (milliseconds)
-- How long after a bonk denial the second tap is still considered
-- a valid override. Default: 1500 ms. Only used when
-- doubleTapOverride = true.
bonking = {
blockTransmission = true,
playBonkTone = true,
doubleTapOverride = true,
doubleTapWindow = 1500,
},
-- ┌──────────────────────────────────────────────────────────────┐
-- │ ALERTS │
-- └──────────────────────────────────────────────────────────────┘
-- Alert definitions used by the SGN button and dispatch panel.
-- The first entry ([1]) is the default alert triggered by the in-game
-- SGN button. Tone names correspond to entries in
-- client/radios/default/tones.json.
--
-- ── Tone configuration ───────────────────────────────────────────
-- Every alert supports two tone styles — pick whichever fits:
--
-- SIMPLE (single field, backward-compatible):
-- tone = "ALERT_A"
-- The same tone is played for every lifecycle phase (activate,
-- repeat, deactivate). Existing configs need no changes.
--
-- PER-PHASE (new tones table, any field is optional):
-- tones = {
-- ["activate"] = "ALERT_A", -- played once when alert goes ON
-- ["repeat"] = "BEEP", -- played each time the persistent
-- -- loop fires (every ~5-10 s)
-- ["deactivate"] = "ALERT_B", -- played once when alert is cleared
-- }
-- Omit any phase to inherit the `tone` fallback, or leave it
-- silent by setting it to "" or omitting it with no `tone` field.
--
-- ── Repeat behaviour ─────────────────────────────────────────────
-- By default the repeat fires every 510 seconds (random).
-- Override with:
-- repeatInterval = 15000 -- exact ms between repeat fires
-- -- omit for the default random cadence
-- repeatShowBanner = false -- true (default) → flash the alert
-- -- name/colour on the radio display
-- -- each time the repeat fires
-- -- false → play the repeat tone only,
-- -- no visual flash on repeat
--
-- ── Deactivation banner ──────────────────────────────────────────
-- By default, clearing a persistent alert shows "RESUME" in green.
-- Override both fields to customise:
-- deactivateLabel = "ALL CLEAR" -- text shown on radio display
-- deactivateColor = "#126300" -- hex colour of that label
--
-- ── Other fields ─────────────────────────────────────────────────
-- name Display text shown on the radio while active
-- color Hex colour of the alert banner
-- isPersistent true → stays active until manually cleared;
-- the repeat tone fires on a loop
-- false → one-shot, plays once and disappears
-- toneOnly true → plays the tone but shows no banner
alerts = {
[1] = {
name = "SIGNAL 100",
color = "#a38718",
isPersistent = true, -- Stays active until manually cleared
-- Per-phase tones: loud triple-beep to activate, quiet single
-- beep on each repeat so it stays noticeable without being
-- overwhelming, and a falling tone to confirm it's cleared.
tones = {
["activate"] = "PRIORITY",
["repeat"] = "PRIORITY_REPEAT",
["deactivate"] = "ALERT_C",
},
repeatInterval = 15000, -- repeat fires every 15 seconds exactly
-- omit this line for the default random 5-10s cadence
repeatShowBanner = false, -- true → flash alert name on radio each repeat
-- false → play the repeat tone only, no visual flash
deactivateLabel = "RESUME", -- text shown on radio when cleared
deactivateColor = "#1a8a38", -- colour of that label
},
[2] = {
name = "SIGNAL 3",
color = "#1852a3",
isPersistent = true,
-- Simple style: same tone for all phases (fully backward-compatible)
tone = "ALERT_A",
},
[3] = {
name = "Ping",
color = "#1852a3",
tone = "ALERT_B", -- non-persistent: only activate phase matters
},
[4] = {
name = "Boop",
color = "#1c4ba3",
toneOnly = true, -- Plays tone without showing an alert on the radio
tone = "BONK",
},
},
-- ┌──────────────────────────────────────────────────────────────┐
-- │ ZONES & CHANNELS │
-- └──────────────────────────────────────────────────────────────┘
-- Each zone contains a set of channels. Players see zones they have
-- access to based on their NAC ID matching the zone's nacIds list.
--
-- Channel fields:
-- name — Display name on the radio
-- type — "conventional" or "trunked"
-- frequency — Frequency in MHz (must be unique across all zones)
-- frequencyRange — (trunked only) { min, max } frequency range
-- coverage — (trunked only) Coverage radius in meters
-- allowedNacs — NAC IDs that can connect AND scan this channel
-- scanAllowedNacs — NAC IDs that can only scan (not connect)
-- gps — GPS blip settings:
-- color — Blip color ID (https://docs.fivem.net/docs/game-references/blips/#blip-colors)
-- visibleToNacs — Which NAC IDs can see this channel's GPS blips
zones = {
[1] = {
name = "California Highway Patrol",
nacIds = { "141", "110" },
Channels = {
[1] = {
name = "DISP",
type = "conventional",
frequency = 154.755,
allowedNacs = { "141" },
scanAllowedNacs = { "110", "200" },
gps = { color = 54, visibleToNacs = { 141 } },
},
[2] = {
name = "C2C",
type = "trunked",
frequency = 856.1125,
frequencyRange = { 856.000, 859.000 },
coverage = 500,
allowedNacs = { "141" },
scanAllowedNacs = { "110", "200" },
gps = { color = 25, visibleToNacs = { 141 } },
},
[3] = {
name = "10-1",
type = "conventional",
frequency = 154.785,
allowedNacs = { "141" },
scanAllowedNacs = { "110", "200" },
gps = { color = 47, visibleToNacs = { 141 } },
},
[4] = {
name = "OPS-1",
type = "conventional",
frequency = 154.815,
allowedNacs = { "141" },
scanAllowedNacs = { "110", "200" },
gps = { color = 40, visibleToNacs = { 141 } },
},
},
},
[2] = {
name = "Los Angeles POLICE DEPARTMENT",
nacIds = { "141" },
Channels = {
[1] = {
name = "DISP",
type = "conventional",
frequency = 460.250,
allowedNacs = { "141" },
scanAllowedNacs = { "110", "200" },
gps = { visibleToNacs = { 141 } },
},
[2] = {
name = "C2C",
type = "trunked",
frequency = 460.325,
frequencyRange = { 460.325, 462.325 },
coverage = 250,
allowedNacs = { "141" },
scanAllowedNacs = { "110", "200" },
gps = { color = 25, visibleToNacs = { 141 } },
},
[3] = {
name = "10-1",
type = "conventional",
frequency = 460.275,
allowedNacs = { "141" },
scanAllowedNacs = { "110", "200" },
gps = { color = 47, visibleToNacs = { 141 } },
},
[4] = {
name = "OPS-1",
type = "conventional",
frequency = 462.450,
allowedNacs = { "50" },
scanAllowedNacs = { "141" },
gps = { color = 40, visibleToNacs = { 141 } },
},
},
},
[3] = {
name = "Los Angeles County Sheriff",
nacIds = { "141" },
Channels = {
[1] = {
name = "DISP",
type = "conventional",
frequency = 155.070,
allowedNacs = { "141" },
scanAllowedNacs = { "110", "200" },
gps = { color = 52, visibleToNacs = { 141 } },
},
[2] = {
name = "C2C",
type = "trunked",
frequency = 155.220,
frequencyRange = { 155.220, 157.220 },
coverage = 250,
allowedNacs = { "141" },
scanAllowedNacs = { "110", "200" },
gps = { color = 25, visibleToNacs = { 141 } },
},
[3] = {
name = "10-1",
type = "conventional",
frequency = 155.100,
allowedNacs = { "141" },
scanAllowedNacs = { "110", "200" },
gps = { color = 47, visibleToNacs = { 141 } },
},
[4] = {
name = "OPS-1",
type = "conventional",
frequency = 157.350,
allowedNacs = { "141" },
scanAllowedNacs = { "110", "200" },
gps = { color = 40, visibleToNacs = { 141 } },
},
},
},
},
-- ┌──────────────────────────────────────────────────────────────┐
-- │ CLIENT CALLBACKS │
-- └──────────────────────────────────────────────────────────────┘
-- These functions run on the CLIENT and are called automatically
-- by the radio system. Modify them to integrate with your framework.
-- Determines whether PTT (Push-to-Talk) is allowed based on player state.
-- Return true to allow talking, false to block the transmission.
-- You can add custom conditions here, e.g.:
-- - Player is cuffed/restrained
-- - Player is performing an animation
-- - Custom framework conditions
talkCheck = function()
if IsPlayerDead(PlayerId()) then
return false
end
if IsPedSwimming(PlayerPedId()) then
return false
end
-- Add your custom conditions here
-- Example: Block transmission when cuffed (uncomment and modify)
-- if exports['your-handcuff-resource']:IsPlayerCuffed() then
-- return false
-- end
return true
end,
-- Checks whether the player's vehicle siren audio is active. Used to
-- play siren background effects during radio transmissions.
--
-- LVC INTEGRATION VERSION (Default)
-- The lvcSirenState parameter is tracked in shared.lua via the
-- lvc:UpdateThirdParty event:
-- 0 = No siren audio (lights only or off)
-- >0 = Siren audio playing (Wail, Yelp, Priority, etc.)
--
-- If you DON'T use LVC (Luxart Vehicle Control), see the non-LVC
-- fallback version commented out below this function.
bgSirenCheck = function(lvcSirenState)
local playerPed = PlayerPedId()
if not playerPed or playerPed == 0 then return false end
local vehicle = GetVehiclePedIsIn(playerPed, false)
if not vehicle or vehicle == 0 then return false end
return lvcSirenState and lvcSirenState > 0
end,
--[[ ── NON-LVC FALLBACK VERSION ─────────────────────────────────
-- Use this if you DON'T have LVC (Luxart Vehicle Control) installed.
-- 1. Comment out the LVC version above.
-- 2. Uncomment this entire block.
--
-- WARNING: Cannot distinguish lights-only from siren audio — may
-- trigger false positives when only emergency lights are on.
bgSirenCheck = function(lvcSirenState)
local playerPed = PlayerPedId()
if not playerPed or playerPed == 0 then return false end
local vehicle = GetVehiclePedIsIn(playerPed, false)
if not vehicle or vehicle == 0 then return false end
if not IsVehicleSirenOn(vehicle) then return false end
local speed = GetEntitySpeed(vehicle) * 2.237 -- m/s to mph
if speed <= 10 then return false end
return IsVehicleSirenOn(vehicle)
end,
--]]
-- ┌──────────────────────────────────────────────────────────────┐
-- │ SERVER CALLBACKS │
-- └──────────────────────────────────────────────────────────────┘
-- These functions run on the SERVER and are called automatically
-- by the radio system. Modify them to integrate with your framework.
-- See the documentation for QBCore, ESX, and other framework examples.
-- Permission check — return true to grant radio access, false to deny.
-- Called when a player connects to the radio system.
-- @param playerId number Server-side player ID
-- @return boolean
radioAccessCheck = function(playerId)
if not playerId or playerId <= 0 then
log("Invalid playerId in radioAccessCheck: " .. tostring(playerId), 0)
return false
end
-- QB-Core example:
-- local player = exports['qb-core']:GetPlayer(playerId)
-- if player and player.PlayerData and player.PlayerData.job then
-- local job = player.PlayerData.job.name
-- return job == "police" or job == "ambulance" or job == "ems"
-- end
-- return false
return true -- Default: allow everyone
end,
-- Returns the NAC ID for a player. NAC IDs control which zones and
-- channels a player can access. Always return a string.
--
-- If you implement dynamic NAC IDs based on job/role, trigger a refresh
-- when the job changes by calling:
-- exports['radio']:refreshNacId(serverId)
--
-- @param serverId number Server-side player ID
-- @return string|nil
getUserNacId = function(serverId)
if not serverId or serverId <= 0 then
return nil
end
-- QB-Core example:
-- local player = exports['qb-core']:GetPlayer(serverId)
-- if player and player.PlayerData and player.PlayerData.job then
-- local job = player.PlayerData.job.name
-- if job == "police" then return "141"
-- elseif job == "ambulance" or job == "ems" then return "200"
-- elseif job == "firefighter" then return "300"
-- end
-- end
-- return "0"
return tostring("141")
end,
-- Returns the display name for a player shown on the radio and
-- dispatch panel.
--
-- Behavior depends on useCallsignSystem:
-- Disabled — This is the ONLY source for display names everywhere.
-- Enabled — This is the DEFAULT/FALLBACK. Once a player sets a
-- custom callsign (via /callsign or dispatch panel), that
-- callsign takes priority. Custom callsigns persist in
-- client KVP across sessions.
--
-- @param serverId number Server-side player ID (0 or nil = dispatch)
-- @return string
getPlayerName = function(serverId)
if not serverId then return "DISPATCH" end
if serverId <= 0 then return "DISPATCH" end
-- QB-Core example:
-- local player = exports['qb-core']:GetPlayer(serverId)
-- if player and player.PlayerData then
-- if player.PlayerData.metadata and player.PlayerData.metadata.callsign
-- and player.PlayerData.metadata.callsign ~= "NO CALLSIGN"
-- and player.PlayerData.metadata.callsign ~= "" then
-- return player.PlayerData.metadata.callsign
-- end
-- if player.PlayerData.charinfo and player.PlayerData.charinfo.lastname
-- and player.PlayerData.charinfo.lastname ~= "" then
-- return player.PlayerData.charinfo.lastname
-- end
-- end
local name = GetPlayerName(serverId)
if not name or name == "" then
return "Player " .. serverId
end
-- Extract callsign from name format "Name S. 2L-319"
local callsign = string.match(name, "%s([%w%-]+%d+)$")
if callsign then
return callsign
end
return name
end,
}