699 lines
34 KiB
Lua
699 lines
34 KiB
Lua
-- 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 = 012019,
|
||
|
||
-- Secure token for radio authentication. Change this to a long, random
|
||
-- string — it protects the voice server and dispatch panel API.
|
||
authToken = " ",
|
||
|
||
-- 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 = " ",
|
||
|
||
-- 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 (80–800) Low cut. Removes rumble/low-end.
|
||
-- 250 Hz is a clean radio sound without
|
||
-- eating the low harmonics of deep voices.
|
||
lowpassFrequency = 3400, -- Hz (1200–8000) High cut. 3400 Hz = telephone band.
|
||
distortion = 20, -- (0–100) Tube saturation / analog warmth.
|
||
compression = 60, -- (0–100) Dynamic range crushing.
|
||
midBoost = 2, -- dB (-12 to 12) Presence boost at ~1200 Hz.
|
||
inputGain = 1.2, -- (0.5–3.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 5–10 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,
|
||
}
|