Configuration

18 min readUpdated 3 weeks ago

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

shared/config.lua
1Config.Debug = false
2Config.Locale = 'en'
3Config.MaxFrequency = 999.99
4Config.disconnectOnDeath = true
  • Debug - Development prints. Leave false in production.
  • Locale - Interface language. Translations live in the locales/ folder.
  • MaxFrequency - Highest tunable frequency.
  • disconnectOnDeath - Drops the player off the air when they die.

Items

shared/config.lua
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 Items

Antenna and jammer models are configured with their own rules in shared/config_coverage.lua, under Config.Antenna.tiers and Config.Jammer.tiers.

Channels

shared/config.lua
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, so 2 also locks 2.1 and 2.55.
  • restrictions[n].requireDuty - Also checks the on-duty state.

Dual Channel

shared/config.lua
1Config.DualChannel = {
2    enabled = true,
3    swapKey = '',
4
5    defaults = {
6        primaryVolume = 100,
7        secondaryVolume = 60
8    }
9}
  • enabled - false hides 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

shared/config.lua
1Config.Battery = {
2    enabled = true,
3    tickInterval = 60000,
4    drainPerMinute = 0.5,
5    chargeAmount = 50,
6    lowThreshold = 20
7}
  • enabled - false means 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

shared/config.lua
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.
Keybinds Ship Unbound

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

shared/config_coverage.lua
1Config.Signal = {
2    enabled = true,
3    baseSignalWithoutAntenna = 0,
4    updateInterval = 1000,
5    cutoffThreshold = 5,
6    regainThreshold = 10,
7    staticSoundOnLoss = true
8}
  • enabled - false gives 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 above cutoffThreshold stops 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.
Three Coverage Setups

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

shared/config_coverage.lua
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. 1 is a straight ramp, 3 stays 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 - true adds 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.
Voice Degradation Needs pma-voice

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

shared/config_coverage.lua
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.0 reads 100 at the antenna and 0 at the edge. 0.5 leaves 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's weatherResistance.
  • thresholds - Where the bars change colour and wording.

Player Antennas

shared/config_coverage.lua
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 - false stops 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 own hackLockDuration.
  • hack.successChance - Roll from 0 to 1. A failure keeps the item unless removeItemOnFail is true.
  • hack.damageOnSuccess - Health chipped off on a landed hack, on top of the lockout. Set 0 for lockout only.
  • hack.requireSkill - Optional. An ml_skills skill the player must have unlocked. Leave it commented out to run without ml_skills.
  • permission - Group allowlists per action, checked server-side. Empty tables allow everyone. place covers putting an antenna down, configure covers switching it on and off, and remove covers picking it back up. Repairing is not gated, so anyone carrying the repair kit can patch a damaged antenna.

Antenna Models

shared/config_coverage.lua
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.
DUI Offset Is Per Model

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

shared/config_coverage.lua
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 - false removes 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 with infiniteHealth.
  • 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

shared/config_coverage.lua
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. place covers putting a jammer down, remove covers picking it back up, and configure covers 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 takes id, itemName, coords as a vec4 with the heading last, range and an optional whitelist of 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

shared/config_audio.lua
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, false allows nobody so the station only ever plays its own autoplay.
  • stations[].allowStreaming - false blocks 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 a url and a title. 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. false leaves the console silent.
  • djMonitor.volume - Volume of that monitor feed, 0 to 1.
Ship Your Own Audio

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.

The Audio Is Yours To Clear

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

shared/config_audio.lua
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

shared/config_audio.lua
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 - false restricts 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 - false blocks players from typing their own stream links on that device.
  • items[].maxQueueTracks - How many tracks the queue holds. 0 on 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 / drainPerMinute is the runtime in minutes, so 0.5 gives 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:

shared/config_audio.lua
1{
2    name = 'pa_speaker',
3    battery = { enabled = true, drainPerMinute = 2.0 }
4}
Switching The Battery On Later

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

server/config_server.lua
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 the INSERT_WEBHOOK_LINK_HERE placeholder, which posts nothing until replaced. Set any webhook to false to disable that log entirely.
  • AllowedExportResources - Non ml_* 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.

The Export Allowlist Is Keyed By Resource Name

AllowedExportResources is looked up by key, not searched as a list. Writing { 'my_dispatch' } denies that resource instead of allowing it.

server/config_server.lua
1Config.AllowedExportResources = {
2    ['my_dispatch'] = true
3}

AllowedStreamHosts and BlockedStreamHosts accept either form:

server/config_server.lua
1Config.AllowedStreamHosts = { 'radio.myserver.com' }
2Config.AllowedStreamHosts = { ['radio.myserver.com'] = true }
Radio Configuration, FiveM Docs | Micio Mods