Configuration

14 min readUpdated 2 days 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 - Logging provider, 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 = 'batteria_stilo',
3    repairKit = 'electronic_kit',
4
5    radios = {
6        {
7            name = 'radio',
8            label = 'Portable Radio',
9            rangeBonus = 0,
10            batteryMax = 100,
11            batteryDrain = 0.5
12        }
13    }
14}
  • 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.

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
6    theme = 'default',
7    scaleFactor = 1.0,
8    defaultPosition = 'bottom-right',
9
10    allowMovement = true,
11    enableMicClicks = true,
12    enablePlayerMute = true
13}
  • keybind - Opens the radio. Empty ships unbound.
  • channelUpKey / channelDownKey - Tune one channel up or down.
  • 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    staticSoundOnLoss = true
7}
  • 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 - Below this the channel goes silent.
  • 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.
  • 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. Empty tables allow everyone.

Antenna Models

shared/config_coverage.lua
1Config.Antenna.tiers = {
2    {
3        name = 'radio_antenna_small',
4        label = 'Portable Antenna',
5        model = 'prop_aerial_01a',
6        signalRange = 500.0,
7        spawnDistance = 100.0,
8        healthMax = 100,
9        weatherResistance = 0,
10        maxPerPlayer = 3,
11
12        hackable = true,
13        repairable = true,
14        toggleable = true,
15        pickupable = true,
16
17        repairItems = {
18            ['electronic_kit'] = { addHealth = 25, duration = 5000 }
19        },
20
21        dui = {
22            enabled = true,
23            renderDistance = 15.0,
24            scale = 0.08,
25            offset = vec3(0.0, 0.0, 1.6)
26        }
27    }
28}
  • 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 = 450.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 450) 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            label = 'Basic Jammer',
22            model = 'sm_prop_smug_jammer',
23            rangeMin = 10.0,
24            rangeMax = 100.0,
25            rangeDefault = 50.0,
26            powerMax = 100,
27            powerConsumption = 10,
28            healthMax = 100,
29            maxPerPlayer = 2
30        }
31    }
32}
  • 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 = true,
22                shuffle = false,
23                defaultDuration = 1800,
24                tracks = {
25                    { url = 'https://ice1.somafm.com/groovesalad-128-mp3', title = 'Groove Salad' }
26                }
27            }
28        }
29    },
30
31    ducking = {
32        enabled = true,
33        voiceVolume = 0.1
34    }
35}

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. An empty table means nobody can, so the station only ever plays its 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.
  • autoplay.shuffle - Picks tracks at random.
  • autoplay.defaultDuration - Seconds a track is assumed to last when the stream never ends.
  • ducking.voiceVolume - Music volume while someone talks on the frequency.
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.

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
6    items = {
7        {
8            name = 'boombox',
9            label = 'Boombox',
10            model = 'prop_ghettoblast_01',
11            maxDistance = 25.0,
12            volumeDefault = 0.6,
13            maxPerPlayer = 2
14        }
15    },
16
17    interaction = {
18        distance = 2.0,
19        icon = 'fa-radio'
20    },
21
22    presets = {
23        { label = 'Radio Los Santos', freq = 88.4 }
24    },
25
26    battery = {
27        enabled = false,
28        startLevel = 100,
29        rechargeItem = 'batteria_stilo',
30        rechargeAmount = 50,
31        drainPerMinute = 0.5
32    }
33}
  • publicControl - false restricts power, tuning and volume to the owner.
  • pickupBy - 'owner' or 'all'.
  • items[].maxDistance - Metres the 3D audio reaches.
  • 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.

Server Config

server/config_server.lua
1Config.Logger = 'discord'
2
3Config.DiscordWebhook = {
4    ChannelJoin = 'INSERT_WEBHOOK_LINK_HERE',
5    ChannelLeave = 'INSERT_WEBHOOK_LINK_HERE',
6    JammerPlaced = 'INSERT_WEBHOOK_LINK_HERE',
7    ExtenderPlaced = 'INSERT_WEBHOOK_LINK_HERE',
8    BroadcastStart = 'INSERT_WEBHOOK_LINK_HERE',
9    ExploitDetected = 'INSERT_WEBHOOK_LINK_HERE',
10    HackSuccess = 'INSERT_WEBHOOK_LINK_HERE',
11}
12
13Config.LogSettings = {
14    Color = 3066993,
15    ExploitColor = 15548997,
16    Interval = 60000,
17    AuthorIcon = 'https://r2.fivemanage.com/vflg1Fv0RSFZkCUDGbmB1/miciomods2.png',
18    AuthorName = 'ML Radio | Logs'
19}
20
21Config.AllowedExportResources = {}
22Config.AllowedStreamHosts = {}
  • Logger - Where log entries go.
  • DiscordWebhook - One webhook per event category. Each ships as the INSERT_WEBHOOK_LINK_HERE placeholder, which posts nothing until replaced. ExploitDetected is the one to fill first: it fires when a client sends something the server rejects. Set any webhook to false to disable that log entirely.
  • LogSettings.Color / ExploitColor - Embed side colour as a decimal integer, normal and exploit.
  • LogSettings.Interval - Milliseconds the queued log batch waits before it is flushed.
  • LogSettings.AuthorIcon / AuthorName - Avatar and name on the embed.
  • AllowedExportResources - Non ml_* resources allowed to call mutating exports. ml_* resources are allowed automatically.
  • AllowedStreamHosts - Restricts DJ stream links to specific hosts. Empty allows any public host, and private addresses are always blocked.
Both Allowlists Are Keyed Tables

These two are looked up by key, not searched as a list. Writing { 'ice1.somafm.com' } blocks every stream instead of allowing that one.

server/config_server.lua
1Config.AllowedStreamHosts = {
2    ['ice1.somafm.com'] = true
3}
4
5Config.AllowedExportResources = {
6    ['my_dispatch'] = true
7}