Sistema de radio y despacho para FiveM: PMA, pma-voice y CAD
Configura sistemas de radio, despacho y CAD para FiveM. Canales de pma-voice, herramientas MDT y scripts para equipos policiales, médicos y mecánicos.
Agency Scripts
Fundador y desarrollador principal en Agency Scripts
Arquitectura del sistema de radio y despacho
Un sistema de radio y despacho es la columna vertebral de las comunicaciones de los servicios de emergencia en un servidor de roleplay de FiveM. Permite que la policía, los EMS y los bomberos coordinen sus respuestas mediante canales de radio estructurados, reciban alertas de despacho priorizadas de llamadas al 911 y sistemas de detección automatizados, y sigan la ubicación de las unidades en tiempo real mediante la integración GPS. La arquitectura consta de tres sistemas interconectados: el sistema de canales de radio, que gestiona quién puede hablar en cada frecuencia; la cola de despacho, que procesa las llamadas entrantes y las dirige a las unidades adecuadas; y la capa de seguimiento GPS, que muestra las ubicaciones de las unidades en un mapa compartido. A diferencia de una comunicación sencilla por chat, un sistema de radio adecuado aporta la inmersión de cambiar de frecuencia, oír las comunicaciones de radio solo cuando estás sintonizado y seguir protocolos de comunicación estructurados que reflejan los servicios de emergencia del mundo real. Todo el sistema se gestiona desde el servidor para evitar trampas, mientras que el NUI del cliente proporciona la interfaz de radio y el panel de despacho.
Sistema de canales de radio
Los canales de radio organizan las comunicaciones para que los distintos departamentos y unidades puedan coordinarse sin interferencias. Cada canal tiene un número de frecuencia, restricciones de acceso según los roles laborales y una lista de los jugadores conectados en ese momento. El sistema admite varios tipos de canales: canales para todo el departamento destinados a la comunicación general, canales tácticos para operaciones específicas como una persecución o la respuesta a un robo de banco y canales entre departamentos que permiten que la policía y los EMS coordinen incidentes compartidos. Los jugadores se unen a un canal mediante un objeto de radio o una interfaz NUI, y toda voz o texto transmitido en ese canal llega únicamente a los jugadores sintonizados en la misma frecuencia. Implementa un sistema de prioridad para emisores en el que los operadores de despacho y los oficiales al mando puedan transmitir simultáneamente a todos los canales para enviar alertas de emergencia:
Config.RadioChannels = {
-- Police Department
{ frequency = 1, label = 'PD Main', jobs = {'police'}, type = 'department' },
{ frequency = 2, label = 'PD Patrol', jobs = {'police'}, type = 'department' },
{ frequency = 3, label = 'PD Tactical', jobs = {'police'}, type = 'tactical', maxUsers = 8 },
{ frequency = 4, label = 'PD Detectives', jobs = {'police'}, type = 'tactical', minRank = 3 },
-- EMS / Fire
{ frequency = 10, label = 'EMS Main', jobs = {'ambulance'}, type = 'department' },
{ frequency = 11, label = 'EMS Field', jobs = {'ambulance'}, type = 'department' },
{ frequency = 12, label = 'Fire Main', jobs = {'fire'}, type = 'department' },
-- Inter-department
{ frequency = 20, label = 'Emergency Joint', jobs = {'police','ambulance','fire'}, type = 'inter' },
{ frequency = 21, label = 'Command Channel', jobs = {'police','ambulance','fire'}, type = 'command', minRank = 5 },
-- Civilian (if radio item owned)
{ frequency = 50, label = 'Civilian Band', jobs = {}, type = 'open' },
}
-- Server-side channel state
local radioState = {} -- [frequency] = { players = {}, priority = false }
RegisterNetEvent('radio:server:joinChannel', function(frequency)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
local channel = nil
for _, ch in ipairs(Config.RadioChannels) do
if ch.frequency == frequency then channel = ch break end
end
if not channel then return end
-- Check job access
if #channel.jobs > 0 then
local playerJob = Player.PlayerData.job.name
local hasAccess = false
for _, job in ipairs(channel.jobs) do
if job == playerJob then hasAccess = true break end
end
if not hasAccess then
TriggerClientEvent('QBCore:Notify', src, 'No access to this frequency', 'error')
return
end
end
-- Check rank requirement
if channel.minRank and Player.PlayerData.job.grade.level < channel.minRank then
TriggerClientEvent('QBCore:Notify', src, 'Insufficient rank for this channel', 'error')
return
end
-- Check max users for tactical channels
if channel.maxUsers and radioState[frequency] then
if #radioState[frequency].players >= channel.maxUsers then
TriggerClientEvent('QBCore:Notify', src, 'Channel full', 'error')
return
end
end
-- Leave current channel
LeaveCurrentChannel(src)
-- Join new channel
if not radioState[frequency] then
radioState[frequency] = { players = {}, priority = false }
end
table.insert(radioState[frequency].players, src)
TriggerClientEvent('radio:client:joined', src, frequency, channel.label)
TriggerClientEvent('QBCore:Notify', src, 'Tuned to ' .. channel.label .. ' (' .. frequency .. ')', 'success')
end)
Sistema de códigos 10
Los códigos diez son abreviaturas de radio estandarizadas que aportan autenticidad a las comunicaciones de los servicios de emergencia y permiten enviar actualizaciones rápidas de estado mediante el sistema de despacho. En lugar de implementar los códigos 10 como simples macros de chat, conviértelos en comandos funcionales que actualicen el estado del agente en el sistema de despacho y activen respuestas automatizadas. Cuando un agente diga 10-80 (persecución en curso), el sistema debe actualizar automáticamente su estado en el panel de despacho, crear una alerta de persecución visible para todas las unidades e iniciar el seguimiento GPS del vehículo del agente. Cuando diga 10-97 (llegando a la escena), el sistema de despacho lo marcará como presente en la escena de la llamada activa a la que está respondiendo. Implementa el sistema de códigos 10 como una tabla configurable para que los propietarios de servidores puedan personalizar los códigos según el protocolo preferido de su departamento:
Config.TenCodes = {
['10-4'] = { label = 'Acknowledged', action = nil, status = nil },
['10-6'] = { label = 'Busy', action = nil, status = 'busy' },
['10-7'] = { label = 'Out of Service', action = 'setOffDuty', status = 'off_duty' },
['10-8'] = { label = 'In Service', action = 'setOnDuty', status = 'available' },
['10-11'] = { label = 'Traffic Stop', action = 'createTrafficStop', status = 'traffic_stop' },
['10-15'] = { label = 'Suspect in Custody', action = nil, status = 'transport' },
['10-20'] = { label = 'Location Request', action = 'shareLocation', status = nil },
['10-23'] = { label = 'Arrived at Scene', action = 'markOnScene', status = 'on_scene' },
['10-32'] = { label = 'Person with Weapon', action = 'createAlert', status = 'responding', priority = 'high' },
['10-41'] = { label = 'Beginning Tour of Duty', action = 'clockIn', status = 'available' },
['10-42'] = { label = 'Ending Tour of Duty', action = 'clockOut', status = 'off_duty' },
['10-71'] = { label = 'Shooting', action = 'createAlert', status = 'responding', priority = 'critical' },
['10-78'] = { label = 'Officer Needs Assistance', action = 'panicButton', status = 'emergency', priority = 'critical' },
['10-80'] = { label = 'Pursuit in Progress', action = 'startPursuit', status = 'pursuit', priority = 'high' },
['10-97'] = { label = 'Arriving on Scene', action = 'markOnScene', status = 'on_scene' },
['10-99'] = { label = 'Officer Down', action = 'officerDown', status = 'emergency', priority = 'critical' },
['code4'] = { label = 'No Further Assistance', action = 'clearScene', status = 'available' },
}
RegisterNetEvent('radio:server:tenCode', function(code)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
local codeConfig = Config.TenCodes[code]
if not codeConfig then return end
-- Update officer status
if codeConfig.status then
UpdateUnitStatus(src, codeConfig.status)
end
-- Execute associated action
if codeConfig.action then
ExecuteCodeAction(codeConfig.action, src, codeConfig)
end
-- Broadcast to channel
local channel = GetPlayerChannel(src)
if channel then
local name = GetUnitCallsign(src) or Player.PlayerData.charinfo.firstname
BroadcastToChannel(channel, {
type = 'tencode',
code = code,
label = codeConfig.label,
unit = name,
priority = codeConfig.priority,
})
end
end)
Cola de llamadas de despacho
La cola de despacho es el centro que procesa todas las llamadas de emergencia entrantes y las asigna a las unidades adecuadas. Cuando un civil llama al 911, se activa una alarma automática en un banco o una tienda, o un agente de policía solicita refuerzos, el sistema crea una entrada de despacho con un nivel de prioridad, una ubicación, una descripción y el tipo de servicio necesario. La cola muestra todas las llamadas activas ordenadas por prioridad en el panel NUI de despacho, con la hora de la llamada, la ubicación, el estado y las unidades asignadas. Un rol exclusivo de operador de despacho puede asignar manualmente las llamadas a unidades específicas, o los agentes pueden asignárselas ellos mismos haciendo clic en una llamada e indicando que responden. Implementa la escalada automática de llamadas, de modo que las llamadas sin respuesta aumenten de prioridad después de un tiempo de espera configurable, para garantizar que ninguna emergencia quede desatendida en periodos de mucho trabajo:
local dispatchQueue = {}
local callIdCounter = 0
function CreateDispatchCall(data)
callIdCounter = callIdCounter + 1
local call = {
id = callIdCounter,
type = data.type or 'general', -- police, ems, fire, general
priority = data.priority or 'standard', -- low, standard, urgent, critical
code = data.code or nil, -- e.g., '10-71', '211' (robbery)
title = data.title,
description = data.description or '',
coords = data.coords,
street = GetStreetName(data.coords),
blip = data.blip or nil,
caller = data.caller or 'Unknown',
callerId = data.callerId or nil,
timestamp = os.time(),
status = 'pending', -- pending, assigned, responding, on_scene, resolved
assignedUnits = {},
escalationTimer = nil,
metadata = data.metadata or {},
}
-- Set escalation timer
if call.priority ~= 'critical' then
call.escalationTimer = os.time() + Config.EscalationTimeout
end
table.insert(dispatchQueue, call)
-- Notify appropriate department
local targetJobs = GetJobsForCallType(call.type)
local players = QBCore.Functions.GetQBPlayers()
for _, player in pairs(players) do
local job = player.PlayerData.job
if job.onduty and TableContains(targetJobs, job.name) then
TriggerClientEvent('dispatch:client:newCall', player.PlayerData.source, call)
end
end
-- Play alert sound based on priority
if call.priority == 'critical' then
PlayDispatchAlert('critical', targetJobs)
end
return call.id
end
-- Automated dispatch triggers
AddEventHandler('banking:robberyStarted', function(bankId, coords)
CreateDispatchCall({
type = 'police',
priority = 'critical',
code = '211',
title = 'Bank Robbery in Progress',
description = 'Silent alarm triggered at ' .. GetBankLabel(bankId),
coords = coords,
blip = { sprite = 500, color = 1, scale = 1.5, flash = true },
})
end)
AddEventHandler('hospital:911call', function(callerId, coords, description, injuries)
CreateDispatchCall({
type = 'ems',
priority = injuries and injuries.maxSeverity >= 80 and 'critical' or 'urgent',
title = 'Medical Emergency',
description = description,
coords = coords,
callerId = callerId,
caller = GetPlayerName(callerId),
blip = { sprite = 153, color = 3, scale = 1.2 },
metadata = { injuries = injuries },
})
end)
Seguimiento GPS y mapa de unidades
El seguimiento GPS ofrece visibilidad en tiempo real de la ubicación de todas las unidades activas en el mapa, lo que permite a los operadores de despacho y a los oficiales al mando tomar decisiones informadas al asignarlas. La posición de cada agente de servicio se transmite al sistema de despacho a intervalos configurables, normalmente cada 5-10 segundos, y se muestra en un panel de mapa NUI compartido, accesible para operadores de despacho y supervisores. El mapa muestra marcadores de unidades con colores según el departamento y el estado, e iconos que indican si están disponibles, respondiendo, en la escena o en una persecución. Incluye una función de rastro que registra el movimiento de las unidades durante los últimos minutos para que los operadores de despacho puedan ver la dirección de desplazamiento durante las persecuciones. Implementa una visualización del radio de llamada que dibuje un círculo alrededor de las llamadas de despacho activas y muestre el área de respuesta recomendada. Cuando un operador de despacho necesite encontrar la unidad disponible más cercana a una nueva llamada, el sistema puede calcular las distancias de todas las unidades disponibles y sugerir la asignación óptima. Mantén eficiente la transmisión GPS enviando actualizaciones de posición solo cuando la unidad se haya desplazado más allá de una distancia mínima configurable, para reducir el tráfico de red en servidores con mucha actividad.
Sistema de alertas y botón de pánico
El sistema de alertas gestiona notificaciones de alta prioridad que requieren la atención inmediata de todas las unidades. La alerta más crítica es el botón de pánico, que se activa cuando un agente llama 10-78 o 10-99y envía su ubicación GPS a todas las unidades de servicio con un marcador parpadeante y un sonido de alarma distintivo. La alerta de pánico debe tener prioridad sobre las llamadas normales de la cola de despacho y mostrarse de forma destacada en la pantalla de cada agente hasta que la confirme. Implementa distintos niveles de alerta que activen respuestas progresivas: una alerta estándar crea una llamada de despacho con un sonido de notificación, una alerta urgente hace parpadear el panel de despacho y reproduce un tono de advertencia, y una alerta crítica muestra una notificación a pantalla completa con sonido de sirena y navegación GPS automática hasta la ubicación de la alerta. Integra las alertas automáticas con otros sistemas del servidor para que las zonas de detección de disparos generen alertas cuando se disparen armas en zonas públicas, las cámaras de velocidad detecten vehículos que superen los límites de velocidad y los sistemas de alarma de tiendas creen llamadas de despacho cuando empiecen los robos. Cada tipo de alerta tiene tiempos de espera configurables para evitar el spam de activaciones repetidas en la misma zona.
Rol de operador de despacho
El operador de despacho es un rol exclusivo que trabaja en una estación de trabajo y gestiona el flujo de llamadas de emergencia a las unidades en el terreno. A diferencia de los agentes, que ven una notificación de despacho simplificada, el operador dispone de una consola NUI de despacho completa, con un gestor de cola, una lista de unidades, un mapa en vivo y herramientas de comunicación. El operador puede priorizar llamadas arrastrándolas en la cola, asignarlas a unidades específicas según su proximidad y disponibilidad, marcarlas como resueltas cuando las unidades informen que están libres y transmitir mensajes a todas las unidades en una frecuencia del departamento. Implementa un sistema de estaciones de trabajo que requiera que los operadores de despacho estén sentados en un escritorio de despacho designado para acceder a la consola completa, y así evitar que operen el sistema de despacho mientras están en el terreno. El rol de operador debe tener su propia progresión de grados laborales, donde los operadores con más experiencia obtengan acceso a herramientas de coordinación entre departamentos y capacidades de difusión de emergencias. Registra métricas de rendimiento del despacho, como el tiempo medio de respuesta, las llamadas atendidas por turno y la tasa de resolución de llamadas, para dar a los operadores información sobre su rendimiento y proporcionar a los administradores del servidor datos que les ayuden a tomar decisiones de personal.
Integración de radio de voz
En los servidores que usan soluciones de chat de voz como pma-voice o mumble-voip, integra el sistema de radio con la voz por proximidad para crear comunicaciones de radio realistas. Cuando un jugador transmite en un canal de radio al mantener pulsada una tecla asignada, todos los jugadores en la misma frecuencia deben oír su voz independientemente de la proximidad física, con un efecto de filtro de radio que distinga la comunicación por radio de las conversaciones cara a cara. Implementa un sistema de silenciamiento en el que los jugadores deban soltar la tecla de transmisión antes de que otro jugador pueda hablar, para simular el funcionamiento semidúplex de las radios reales. Añade efectos de sonido de radio al activar y dejar de transmitir para indicar cuándo alguien empieza y termina de hablar. La integración de voz debe respetar los controles de acceso a los canales definidos en el sistema de radio, de modo que los jugadores que no se hayan unido a una frecuencia a través de la NUI de radio no puedan oír ni transmitir en ella, aunque de algún modo conozcan el ID del canal de mumble. En los servidores sin chat de voz, ofrece un sistema de radio basado en texto como alternativa, donde los mensajes enviados en una frecuencia aparezcan en un formato de chat personalizado con el indicativo del remitente y el identificador del canal.
Integración y extensibilidad
Tu sistema de radio y despacho debe ofrecer una API clara que otros recursos puedan utilizar para crear llamadas de despacho, enviar alertas y consultar el estado de las unidades sin tener que conocer la implementación interna. Exporta funciones como CreateDispatchCall, SendAlert, GetAvailableUnits, y GetUnitStatus para que cualquier recurso del servidor pueda integrarse con el sistema de despacho. Cuando empieza un atraco a un banco, el recurso bancario llama al export de despacho para crear una llamada. Cuando EMS recibe una llamada al 911, el recurso del hospital la envía a través del mismo sistema de despacho. Este enfoque centralizado garantiza que todas las comunicaciones de emergencia pasen por un único sistema con gestión de prioridades, seguimiento de unidades y registros coherentes. Guarda el historial de llamadas de despacho en la base de datos para que los administradores puedan revisarlo y genera informes de turno que resuman el volumen de llamadas, los tiempos de respuesta y la actividad de las unidades en cada periodo de servicio. Conecta el sistema de despacho a webhooks de Discord para que las alertas críticas, como las llamadas de agente caído o los atracos a bancos, aparezcan en un canal exclusivo para el personal y mantengan informados a los administradores del servidor aunque no estén en el juego.