Skip to content

Radio Station Framework

Other resources — TVs, venues, jukebox props, business MLOs — can put the city radio on their own speakers by owning a static emitter programmatically. A static emitter is a fixed point (or interior) that plays a configured station with the same distance falloff, interior muffling and mixing every other radio in the city uses.

All integration exports are server-side and live on the rs_radio resource. There are no permissions involved: exports are resource-to-resource calls and are trusted like any other export.

createStaticEmitter(spec) -> id | nil, error

Section titled “createStaticEmitter(spec) -> id | nil, error”

Creates a persistent static emitter and returns its numeric id.

local id = exports.rs_radio:createStaticEmitter({
label = "paddock_tv_3", -- your stable name for lookup (max 80 chars)
pos = { x = 215.2, y = -1002.4, z = 29.3 }, -- or a vector3
stationId = "south", -- any station id configured on this server
radius = 20.0, -- 5.0 .. 250.0
volume = 0.4, -- 0.0 .. 1.0
interiorOnly = true, -- true: audio stays inside the interior
enabled = false -- start switched off (default true)
})

Returns nil, "invalid_spec" for bad input, or nil, "static_emitters_disabled" when the server has disabled the feature.

Emitters persist across restarts exactly like admin-placed ones, so create once and reuse — see the idempotent pattern below.

setStaticEmitterEnabled(id, enabled) -> boolean

Section titled “setStaticEmitterEnabled(id, enabled) -> boolean”

The toggle. Instantly switches the emitter’s audio on or off for every nearby player. Returns false if the id does not exist.

setStaticEmitterStation(id, stationId) -> boolean

Section titled “setStaticEmitterStation(id, stationId) -> boolean”

Retunes the emitter to another configured station (a station picker in your own UI, for example).

Deletes the emitter permanently.

findStaticEmitterByLabel(label) -> id | nil

Section titled “findStaticEmitterByLabel(label) -> id | nil”

Looks up an emitter you created earlier by its exact label.

Create on first use, look up on every start after that, toggle at will:

-- server-side, in your resource
local emitterIds = {}
local function ensureEmitter(key, coords)
local label = ("mytv_%s"):format(key)
local id = exports.rs_radio:findStaticEmitterByLabel(label)
or exports.rs_radio:createStaticEmitter({
label = label,
pos = coords,
stationId = "south",
radius = 18.0,
volume = 0.4,
interiorOnly = true,
enabled = false
})
emitterIds[key] = id
return id
end
-- wire this to your panel's "Radio" button
RegisterNetEvent("mytv:setRadioMode", function(key, coords, on)
local id = emitterIds[key] or ensureEmitter(key, coords)
if id then
exports.rs_radio:setStaticEmitterEnabled(id, on == true)
end
end)

Practical notes:

  • While your radio mode is on, stop your own screen audio for that TV — two mixes fighting sounds broken. rs_radio handles all playback, sync, falloff and interior acoustics for the emitter on its own.
  • stationId must match a station configured on the host server (server owners manage stations in the Station Creator). An unknown id falls back to the default station.
  • Wrap calls in a GetResourceState("rs_radio") == "started" check if your resource should also run on servers without the radio.
  • Admin-placed and integration-created emitters share one list; server admins can see and manage yours with the usual emitter admin tools.

Servers that model stolen, salvaged or mechanic-installed car radios can make the vehicle radio conditional. Turn the gate on in config.lua:

Config.RequireInstalledVehicleRadio = true

Off (the default) every vehicle behaves as it always has, so switching this on is the only thing that changes behaviour.

Installs are keyed by number plate, not network id, so they survive the vehicle despawning, the server restarting and whichever garage script you run. rs_radio never needs to know your garage or mechanic resource exists.

The check is server-authoritative and runs where the vehicle emitter is switched on, so a client cannot talk its way past it. Switching a radio off is always allowed — otherwise stealing a radio would strand the emitter on.

Checked in this order; the first one that has an opinion wins.

The “fit it once and it stays fitted” model. Call it from your mechanic job:

-- server-side, when the mechanic finishes the install
exports.rs_radio:setVehicleRadioInstalled(plate, true, mechanicIdentifier)
-- when a thief rips it out
exports.rs_radio:setVehicleRadioInstalled(plate, false)

Accepts a plate string or a vehicle network id. Persists to rs_radio_vehicle_radios, created automatically on first use.

exports.rs_radio:clearVehicleRadioInstalled(plate)

Forgets the vehicle entirely and hands the answer back to the state bag or your export.

If you already track the radio on the vehicle, just set it server-side:

Entity(vehicle).state:set("rsRadioInstalled", true, true)

For servers whose answer already lives in their own database:

Config.VehicleRadioAccessExport = {
resource = "my_mechanic",
name = "HasInstalledRadio"
}

rs_radio calls it server-side as HasInstalledRadio(netId, plate, entity) and expects a boolean. Results are cached per plate for 30 seconds, so a busy street will not turn into a query storm — call setVehicleRadioInstalled or clearVehicleRadioInstalled when your own state changes and the cache clears immediately.

For a mechanic UI, or to debug a report:

local installed, reason = exports.rs_radio:isVehicleRadioInstalled(plateOrNetId)

reason is one of gate_disabled, rs_radio_store, state_bag, access_export or no_record, which tells you which of the three answered.

A denied toggle notifies with the notify.vehicle_radio_not_installed locale key, translatable in shared/locales.lua like every other message.

With the gate on and nothing claiming a vehicle, the answer is “no radio”. If you want the opposite default — every car has a radio unless someone says otherwise — leave the gate off and remove radios from specific vehicles in your own script instead.

Boombox and Walkboy items are given and taken through bridge/inventory.lua, which is shipped editable (not escrowed). Built in: ox_inventory, qb-inventory, qs-inventory, and the bare framework item functions.

Config.InventoryBridge in config.lua:

ValueBehaviour
"auto" (default)first running one of ox_inventory, qb-inventory, qs-inventory; otherwise framework
"ox_inventory", "qb-inventory", "qs-inventory", "framework"force that adapter
"custom"use the custom adapter you filled in at the bottom of bridge/inventory.lua

The custom adapter needs addItem(src, item, count), removeItem(src, item, count) and getItemCount(src, item); add registerUsableItem(item, callback) only if your inventory does not forward usable items to the framework. The console prints [rs_radio] inventory bridge: <name> on start so you can see which one is active.