Configuration
Overview
shared/config.lua- Language, items, channels, dual channel, battery, interface.shared/config_coverage.lua- Signal model, player antennas, fixed towers, jammers.shared/config_audio.lua- Broadcast stations, speaker zones, placeable stereos.server/config_server.lua- Discord webhooks, export allowlist, stream host allowlist.
Stations, speaker zones and fixed towers load on resource start. Changing them requires a resource restart.
Core
1Config.Debug = false
2Config.Locale = 'en'
3Config.MaxFrequency = 999.99
4Config.disconnectOnDeath = trueDebug- Development prints. Leavefalsein production.Locale- Interface language. Translations live in thelocales/folder.MaxFrequency- Highest tunable frequency.disconnectOnDeath- Drops the player off the air when they die.
Items
1Config.Items = {
2 battery = 'battery_pack',
3 repairKit = 'electronic_kit',
4
5 radios = {
6 {
7 name = 'radio',
8 rangeBonus = 0,
9 batteryMax = 100,
10 batteryDrain = 0.5
11 }
12 }
13}battery- Item that recharges a handset and refuels placed jammers.repairKit- Item that repairs placed antennas and jammers.radios[].name- Inventory item name. Using it opens the interface.radios[].rangeBonus- Metres added on top of the signal reach for this handset.radios[].batteryMax- Battery capacity in percent.radios[].batteryDrain- Percent drained per minute while the radio is on, overrides the global rate.
Add an entry to sell a second handset with a longer range or a bigger battery.
Radios, antennas, jammers and stereos take their display name from the inventory item. Rename one in your items file and it changes in the menus, on the target prompt and on the placed prop.
Antenna and jammer models are configured with their own rules in shared/config_coverage.lua, under Config.Antenna.tiers and Config.Jammer.tiers.
Channels
1Config.Channels = {
2 names = {
3 ['1'] = 'LSPD CH#1',
4 ['1.%'] = 'LSPD Private',
5 ['2'] = 'EMS Dispatch',
6 ['2.%'] = 'EMS Private',
7 ['88.4'] = 'Radio Los Santos'
8 },
9
10 restrictions = {
11 [1] = { type = 'job', names = { 'police', 'lspd' }, requireDuty = true },
12 [2] = { type = 'job', names = { 'ambulance', 'ems' }, requireDuty = true }
13 }
14}names- Display labels. The.%pattern names every sub-channel of a band (1.1, 1.2 and so on).restrictions[n].type-'job'or'gang'.restrictions[n].names- Groups allowed to tune to that base channel. The key locks the whole band, so2also locks 2.1 and 2.55.restrictions[n].requireDuty- Also checks the on-duty state.
Dual Channel
1Config.DualChannel = {
2 enabled = true,
3 swapKey = '',
4
5 defaults = {
6 primaryVolume = 100,
7 secondaryVolume = 60
8 }
9}enabled-falsehides the second channel everywhere.swapKey- Swaps the two channels. Empty ships unbound.defaults.primaryVolume- Starting volume of the main channel, 0 to 100.defaults.secondaryVolume- Starting volume of the background channel.
Battery
1Config.Battery = {
2 enabled = true,
3 tickInterval = 60000,
4 drainPerMinute = 0.5,
5 chargeAmount = 50,
6 lowThreshold = 20
7}enabled-falsemeans the radio never runs out.tickInterval- Milliseconds between server-side drain passes.drainPerMinute- Percent lost per minute. A radio model can override it.chargeAmount- Percent restored per battery item consumed.lowThreshold- Percent that fires the low-battery warning once.
Interface
1Config.UI = {
2 keybind = '',
3 channelUpKey = '',
4 channelDownKey = '',
5 quickSwitchKey = '',
6
7 theme = 'default',
8 scaleFactor = 1.0,
9 defaultPosition = 'bottom-right',
10
11 allowMovement = true,
12 enableMicClicks = true,
13 enablePlayerMute = true
14}keybind- Opens the radio. Empty ships unbound.channelUpKey/channelDownKey- Tune one channel up or down.quickSwitchKey- Jumps the main channel to the next saved favourite, skipping any the player is not allowed on.theme-'default','wasteland','cyberpunk','noir'or'fantasy'.scaleFactor- Multiplier on the size picked from the screen resolution.defaultPosition- First-launch corner:'top-left','top-right','bottom-left','bottom-right'or'center'.allowMovement- Lets the player walk while the radio is open.enableMicClicks- Click sounds when the player starts and stops talking.enablePlayerMute- Per-player mute entries in the channel member list.
Every keybind defaults to an empty string so the resource never takes a key on install. They are listed in Settings, Key Bindings, FiveM for players to assign. Using the radio item opens the interface with no keybind at all. Players who drag the panel off screen restore it with /resetradioui.
Signal
1Config.Signal = {
2 enabled = true,
3 baseSignalWithoutAntenna = 0,
4 updateInterval = 1000,
5 cutoffThreshold = 5,
6 regainThreshold = 10,
7 staticSoundOnLoss = true
8}enabled-falsegives every radio full bars everywhere and turns antennas into decoration.baseSignalWithoutAntenna- Signal a player gets with no antenna in reach, 0 to 100.updateInterval- Milliseconds between recalculations, and how often the bars move.cutoffThreshold- Signal percent below which the channel goes silent.regainThreshold- Signal percent that brings the channel back. Keeping it abovecutoffThresholdstops a player sitting at the edge of range from flipping in and out.staticSoundOnLoss- Plays a burst of static when the signal drops or comes back.
Survival - FixedAntennas.enabled = true with towers covering only part of the map, infiniteHealth = false on them, FixedAntennas.decay.enabled = true and baseSignalWithoutAntenna = 0. Coverage rots unless players repair it.
Light - The same towers with infiniteHealth = true, decay off and baseSignalWithoutAntenna raised to 30 or 50. A bare radio always works, just badly.
Player built - FixedAntennas.enabled = false and baseSignalWithoutAntenna = 0. Nothing transmits until players raise their own antennas.
Interference
1Config.Signal.interference = {
2 enabled = true,
3 degradeBelow = 75,
4 curve = 2.0,
5
6 minMusicVolume = 0.3,
7 maxStaticVolume = 0.18,
8 staticWithoutBroadcast = false,
9
10 voice = {
11 enabled = true,
12 freqLow = 700.0,
13 freqHigh = 2200.0,
14 fudge = 0.7,
15 modFreq = 120.0,
16 mix = 0.55
17 }
18}Audio falls apart gradually instead of cutting out at the threshold. Music fades down, a bed of static fades in, and other people's voices get crunchier.
degradeBelow- Signal percent where degradation starts. Above it the audio is clean. 40 keeps it clean until the edge of range, 80 degrades almost everywhere.curve- How fast it gets bad once it starts.1is a straight ramp,3stays nearly clean and collapses at the edge.minMusicVolume- Broadcast volume at the worst usable signal, as a share of the player's own setting.maxStaticVolume- Static volume at the worst usable signal.staticWithoutBroadcast-trueadds hiss on a weak channel even with nothing on air.voice.enabled- Degrades incoming radio voices through a pma-voice submix.voice.freqLow- Low cut reached at the worst signal. Stock is 389, higher sounds thinner.voice.freqHigh- High cut reached at the worst signal. Stock is 3248, lower sounds more muffled.voice.fudge- Raw distortion. Stock is 0.voice.modFreq- Ring modulation, the robotic warble. Stock is 0.voice.mix- How audible that warble is. Stock is 0.16.
The voice block replaces the radio submix through pma-voice. On a clean signal the stock effect is left untouched. If pma-voice is missing, the music and static still degrade and the voice side is skipped.
Degradation
1Config.Signal.degradation = {
2 distance = 1.0,
3 buildings = 15,
4 underground = 50,
5 weather = {
6 rain = 10,
7 thunder = 25
8 }
9}
10
11Config.Signal.thresholds = {
12 excellent = 80,
13 good = 60,
14 poor = 40,
15 weak = 20
16}distance- Share of the signal lost across an antenna's full range.1.0reads 100 at the antenna and 0 at the edge.0.5leaves 50 at the edge.buildings- Points lost while indoors.underground- Points lost below sea level, scaled with depth and reaching the full value 15 metres down. Skipped near the water surface, so swimming does not count.weather.rain/weather.thunder- Points lost in each condition, reduced by the antenna'sweatherResistance.thresholds- Where the bars change colour and wording.
Player Antennas
1Config.Antenna = {
2 enabled = true,
3 maxPerPlayer = 5,
4 spawnDistance = 300.0,
5 interactionDistance = 3.0,
6 saveHealthToItem = true,
7
8 decay = {
9 enabled = false,
10 rate = 1,
11 tickInterval = 60000
12 },
13
14 hack = {
15 enabled = true,
16 defaultItem = 'hacking_device',
17 duration = 15000,
18 lockDuration = 300000,
19 successChance = 0.7,
20 damageOnSuccess = 50,
21 removeItemOnFail = false,
22 -- requireSkill = 'hacking',
23 },
24
25 permission = {
26 place = {},
27 configure = {},
28 remove = {}
29 }
30}enabled-falsestops players placing antennas.maxPerPlayer- Global cap across all models, on top of each model's own cap.spawnDistance- Fallback streaming distance when a model has none of its own.interactionDistance- How close the player must be to open the antenna menu.saveHealthToItem- Writes the remaining condition onto the item on pickup, restored on re-placement.decay.enabled- Placed antennas lose condition on their own. Off by default.decay.rate- Health lost per tick.decay.tickInterval- Milliseconds between ticks, shared with the fixed tower decay.hack.defaultItem- Item required to attempt a hack.hack.duration- Attempt length in milliseconds.hack.lockDuration- Milliseconds the antenna stays offline and locked after a landed hack. A fixed tower can override it with its ownhackLockDuration.hack.successChance- Roll from 0 to 1. A failure keeps the item unlessremoveItemOnFailis true.hack.damageOnSuccess- Health chipped off on a landed hack, on top of the lockout. Set 0 for lockout only.hack.requireSkill- Optional. Anml_skillsskill the player must have unlocked. Leave it commented out to run withoutml_skills.permission- Group allowlists per action, checked server-side. Empty tables allow everyone.placecovers putting an antenna down,configurecovers switching it on and off, andremovecovers picking it back up. Repairing is not gated, so anyone carrying the repair kit can patch a damaged antenna.
Antenna Models
1Config.Antenna.tiers = {
2 {
3 name = 'radio_antenna_small',
4 model = 'prop_aerial_01a',
5 signalRange = 500.0,
6 spawnDistance = 100.0,
7 healthMax = 100,
8 weatherResistance = 0,
9 maxPerPlayer = 3,
10
11 hackable = true,
12 repairable = true,
13 toggleable = true,
14 pickupable = true,
15
16 repairItems = {
17 ['electronic_kit'] = { addHealth = 25, duration = 5000 }
18 },
19
20 dui = {
21 enabled = true,
22 renderDistance = 15.0,
23 scale = 0.08,
24 offset = vec3(0.0, 0.0, 1.6)
25 }
26 }
27}name- Inventory item name. Using it starts placement.signalRange- Coverage radius in metres.spawnDistance- Metres at which the prop streams in and out.weatherResistance- Percent of the weather penalty this model ignores.maxPerPlayer- Cap for this model alone.hackable/repairable/toggleable/pickupable- Each action on or off for this model. All default to on, and each is enforced server-side, not just hidden in the menu.repairItems- Items accepted for repairs, with the health restored and the progress duration.dui- The status display above the prop: on or off, the distance it appears at, its size, and its height above the base of the prop.
The offset height is tied to the prop. Changing model without re-checking the offset leaves the panel floating in the wrong place.
Fixed Towers
1Config.FixedAntennas = {
2 enabled = true,
3 forceOnline = true,
4 spawnDistance = 500.0,
5
6 decay = {
7 enabled = false,
8 rate = 1
9 },
10
11 locations = {
12 {
13 id = 'southside',
14 label = 'South Central Tower',
15 model = 'dgm_antenna',
16 coords = vec3(729.42, -1524.78, 18.60),
17 heading = 317.97,
18 signalRange = 3000.0,
19 infiniteHealth = false,
20 hackable = true,
21 duiEnabled = true
22 }
23 }
24}enabled-falseremoves the fixed towers. Coverage becomes whatever players build.forceOnline- Towers come back switched on after a restart.decay.enabled- Fixed towers wear down. Ignored on towers withinfiniteHealth.locations[].id- Must be unique.locations[].signalRange- Coverage radius in metres.locations[].infiniteHealth- The tower never takes damage and hides the condition bar.locations[].hackable- Opt-in per tower. Fixed towers are not hackable unless set.locations[].duiEnabled- The status panel above the prop.
Also optional per tower: repairable (default true), spawnProp (default true, set false for an invisible signal source), spawnDistance (default 500) and interactDist (default 3.0).
Jammers
1Config.Jammer = {
2 enabled = true,
3
4 permission = {
5 place = {},
6 configure = {},
7 remove = {}
8 },
9
10 defaultJammers = {},
11
12 durability = {
13 enabled = true,
14 decayRate = 2,
15 destroyable = true
16 },
17
18 tiers = {
19 {
20 name = 'radio_jammer',
21 model = 'sm_prop_smug_jammer',
22 rangeMin = 10.0,
23 rangeMax = 100.0,
24 rangeDefault = 50.0,
25 powerMax = 100,
26 powerConsumption = 10,
27 healthMax = 100,
28 maxPerPlayer = 2
29 }
30 }
31}permission- Group allowlists per action, checked server-side. Empty tables allow everyone.placecovers putting a jammer down,removecovers picking it back up, andconfigurecovers everything in its menu: power on and off, the range slider, and adding or removing a frequency from the whitelist.defaultJammers- Jammers spawned with the server that nobody can pick up. Each entry takesid,itemName,coordsas avec4with the heading last,rangeand an optionalwhitelistof frequencies that keep working inside.durability.decayRate- Power lost per minute while running.durability.destroyable- Lets players shoot the prop.tiers[].rangeMin/rangeMax/rangeDefault- Bounds the owner can set from the menu, and the radius it starts on.tiers[].powerMax/powerConsumption- Power tank when new, and how much drains per tick while active.tiers[].healthMax- Condition when new.
Picking a jammer back up writes its remaining power and condition onto the item.
Stations
1Config.Broadcast = {
2 enabled = true,
3 streamingProvider = 'xsound',
4
5 crossfade = {
6 enabled = true,
7 duration = 4,
8 steps = 40
9 },
10
11 stations = {
12 {
13 id = 'radio_station',
14 label = 'Radio Los Santos',
15 coords = vec3(716.85, 2525.68, 73.40),
16 frequency = 88.4,
17 permission = { 'dj', 'media' },
18 allowStreaming = true,
19
20 autoplay = {
21 enabled = false,
22 shuffle = false,
23 defaultDuration = 1800,
24 tracks = {}
25 }
26 }
27 },
28
29 ducking = {
30 enabled = true,
31 voiceVolume = 0.1
32 },
33
34 djMonitor = {
35 enabled = true,
36 volume = 0.5
37 }
38}A station is a sound source tied to a frequency. Radios, speaker zones and stereos on that frequency all follow it.
streamingProvider-'xsound'needs the xsound resource running.'none'switches all streamed audio off.crossfade.duration- Seconds of overlap between two tracks. A station can override it in its own autoplay block.crossfade.steps- Volume steps across the fade.stations[].coords- Where the DJ console sits.stations[].permission- Jobs allowed to take the console.{}allows everyone,falseallows nobody so the station only ever plays its own autoplay.stations[].allowStreaming-falseblocks the DJ from adding his own links, enforced server-side.autoplay.enabled- Plays on its own from server start and resumes by itself when a DJ stops. Ships off, with an empty track list, because the audio is yours to supply.autoplay.shuffle- Picks tracks at random.autoplay.defaultDuration- Seconds a track is assumed to last when the stream never ends.autoplay.tracks- The rotation. Each entry takes aurland atitle. A station with an empty list stays off the air until a DJ goes live, which also leaves any speaker zone or stereo on that frequency silent.ducking.voiceVolume- Music volume while someone talks on the frequency.djMonitor.enabled- Feeds the station audio back to the DJ while the console is open.falseleaves the console silent.djMonitor.volume- Volume of that monitor feed, 0 to 1.
A track takes a stream URL, or a file inside the resource written as sounds/alarm.mp3 with an mp3, ogg or wav extension. A local file loops forever, so whoever tunes in always hears it however short it is. A stream does not loop and moves the station to the next track when it ends. Only paths under sounds/ are accepted.
No music ships inside the resource. What plays on a station is whatever the config points at, and the rights to it sit with the server owner.
Two separate things decide whether a source is usable. The recording: a track licensed through a performing rights organisation stays off limits whether it is streamed or dropped into sounds/, since where the file sits changes nothing. And the service: YouTube, Spotify and the like license playback only inside their own player, so pulling their audio into the game breaks their terms.
Config.BlockedStreamHosts refuses those services out of the box.
Speaker Zones
1Config.SpeakerZones = {
2 enabled = true,
3
4 zones = {
5 {
6 id = 'legion_square',
7 label = 'Legion Square Speakers',
8 coords = vec3(195.0, -935.0, 30.0),
9 radius = 30.0,
10 frequency = 88.4,
11 volume = 0.4
12 }
13 }
14}radius- Metres the sound covers. Volume falls off towards the edge.frequency- The frequency the zone listens to.volume- Loudest volume at the centre, 0 to 1.
A zone has no audio of its own. It points at a frequency, so a zone on a frequency with no station stays silent. To theme an area, add a station with its own autoplay and give the zone that frequency.
Stereos
1Config.StereoProps = {
2 enabled = true,
3 publicControl = false,
4 pickupBy = 'owner',
5 defaultTrackDuration = 240,
6
7 items = {
8 {
9 name = 'boombox',
10 model = 'prop_ghettoblast_01',
11 maxDistance = 25.0,
12 volumeDefault = 0.6,
13 maxPerPlayer = 2,
14 modes = { 'tuner', 'player' },
15 allowCustomUrl = true,
16 maxQueueTracks = 20
17 },
18 {
19 name = 'pa_speaker',
20 model = 'stt_prop_speakerstack_01a',
21 maxDistance = 60.0,
22 volumeDefault = 0.5,
23 maxPerPlayer = 4,
24 modes = { 'tuner' },
25 allowCustomUrl = false,
26 maxQueueTracks = 0
27 }
28 },
29
30 interaction = {
31 distance = 2.0,
32 icon = 'fa-radio'
33 },
34
35 presets = {
36 { label = 'Radio Los Santos', freq = 88.4 }
37 },
38
39 battery = {
40 enabled = false,
41 startLevel = 100,
42 rechargeItem = 'battery_pack',
43 rechargeAmount = 50,
44 drainPerMinute = 0.5
45 }
46}publicControl-falserestricts power, tuning and volume to the owner.pickupBy-'owner'or'all'.defaultTrackDuration- Seconds a queued track is assumed to last when the stream never reports an end.items[].maxDistance- Metres the 3D audio reaches.items[].modes-'tuner'follows a radio frequency,'player'runs its own queue. Listing one mode locks the device to it.items[].allowCustomUrl-falseblocks players from typing their own stream links on that device.items[].maxQueueTracks- How many tracks the queue holds.0on a tuner-only device.interaction- How close the player must be, and the icon on the target.presets- Quick-pick channels in the stereo menu. Typing a frequency stays available.battery- Optional drainable battery for placed stereos, off by default.battery.startLevel- Charge a freshly placed stereo starts on, in percent.battery.drainPerMinute- Percent lost per minute while the stereo is switched on.100 / drainPerMinuteis the runtime in minutes, so0.5gives a little over three hours.battery.rechargeItem/rechargeAmount- Item the owner spends from the stereo menu, and the percent it restores. At zero charge the stereo powers itself off until it is refilled.
Any device can override the block with its own battery table. Only the keys written there replace the shared ones, so one stereo can run on a battery while another stays mains-powered:
1{
2 name = 'pa_speaker',
3 battery = { enabled = true, drainPerMinute = 2.0 }
4}Turning enabled on also applies to stereos that are already on the ground: they pick up startLevel on the next drain pass instead of staying on infinite power. Turning it back off clears the charge, so a stereo that ran flat starts working again.
Server Config
1Config.DiscordWebhook = {
2 ChannelJoin = 'INSERT_WEBHOOK_LINK_HERE',
3 ChannelLeave = 'INSERT_WEBHOOK_LINK_HERE',
4 JammerPlaced = 'INSERT_WEBHOOK_LINK_HERE',
5 BroadcastStart = 'INSERT_WEBHOOK_LINK_HERE',
6 HackSuccess = 'INSERT_WEBHOOK_LINK_HERE',
7 ExploitDetected = 'INSERT_WEBHOOK_LINK_HERE',
8 StereoPlaced = 'INSERT_WEBHOOK_LINK_HERE',
9 StereoPickedUp = 'INSERT_WEBHOOK_LINK_HERE',
10 StereoTrackAdded = 'INSERT_WEBHOOK_LINK_HERE',
11}
12
13Config.AllowedExportResources = {}
14Config.AllowedStreamHosts = {}
15
16Config.BlockedStreamHosts = {
17 'youtube.com',
18 'youtu.be',
19 'googlevideo.com',
20 'spotify.com',
21 'scdn.co',
22 'soundcloud.com',
23 'sndcdn.com',
24 'deezer.com',
25 'music.apple.com',
26 'tidal.com'
27}DiscordWebhook- One webhook per event. Each ships as theINSERT_WEBHOOK_LINK_HEREplaceholder, which posts nothing until replaced. Set any webhook tofalseto disable that log entirely.AllowedExportResources- Nonml_*resources allowed to call mutating exports.ml_*resources are allowed automatically.AllowedStreamHosts- Restricts DJ and stereo stream links to specific hosts, subdomains included. Empty allows any public host, and private addresses are always blocked.BlockedStreamHosts- Hosts refused even when the allowlist is empty, subdomains included. These services license playback only inside their own player, so their audio cannot legitimately be routed through the game. The blocklist is checked before the allowlist, so listing a host in both still refuses it.
ExploitDetected fires when the server rejects a request that a normal client cannot produce, such as an antenna hack reported as finished before the progress bar could have run.
AllowedExportResources is looked up by key, not searched as a list. Writing { 'my_dispatch' } denies that resource instead of allowing it.
1Config.AllowedExportResources = {
2 ['my_dispatch'] = true
3}AllowedStreamHosts and BlockedStreamHosts accept either form:
1Config.AllowedStreamHosts = { 'radio.myserver.com' }
2Config.AllowedStreamHosts = { ['radio.myserver.com'] = true }