Назад к блогу
Tutorial11 мин чтения

FiveM Radio and Dispatch System: PMA, pma-voice и CADs

Настройте радио, диспетчерскую и CAD системы для FiveM. Каналы pma-voice, инструменты MDT и лучшие скрипты для полиции, EMS и механиков.

Agency Scripts

Основатель и ведущий разработчик Agency Scripts

Архитектура системы радио и диспетчеризации

Радио- и диспетчерская система , коммуникационный каркас экстренных служб на ролевом сервере FiveM. Она позволяет полиции, EMS и пожарным координировать действия через структурированные радиоканалы, получать приоритетные оповещения от звонков 911 и автоматических систем обнаружения, а также отслеживать расположение подразделений в реальном времени через GPS. Архитектура состоит из трех взаимосвязанных систем: фреймворка радиоканалов, управляющего, кто может говорить на какой частоте; диспетчерской очереди, обрабатывающей входящие вызовы и направляющей их нужным подразделениям; и слоя GPS-трекинга, визуализирующего позиции на общей карте. В отличие от простого чата, полноценная радиосистема добавляет погружение через переключение частот, слышимость радиопереговоров только при настройке и следование протоколам, имитирующим реальные экстренные службы. Вся система работает через серверное управление состояниями для предотвращения читерства, а клиентская часть NUI предоставляет интерфейс радио и диспетчерскую панель.

Система радиоканалов

Радиоканалы организуют коммуникацию так, чтобы разные отделы и подразделения могли координироваться без помех. Каждый канал имеет номер частоты, ограничения доступа по ролям и список текущих подключенных игроков. Система поддерживает несколько типов каналов: каналы для всего отдела для общей связи, тактические каналы для конкретных операций, таких как преследование или реагирование на ограбление банка, и межведомственные каналы, где полиция и EMS координируются по общим инцидентам. Игроки подключаются к каналу через радио или NUI-интерфейс, и весь голосовой или текстовый трафик на этом канале доступен только игрокам на той же частоте. Реализуйте систему приоритетных говорящих, где операторы диспетчерской и командиры могут одновременно вещать на все каналы для экстренных оповещений:

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)

10-Code System

Ten-codes , это стандартизированные радиокоды, которые добавляют аутентичности коммуникации экстренных служб и позволяют быстро обновлять статус через диспетчерскую систему. Вместо реализации 10-кодов как простых макросов чата создайте их как функциональные команды, которые обновляют статус офицера в диспетчерской системе и запускают автоматические ответы. Когда офицер вызывает 10-80 (преследование в процессе), система должна автоматически обновить их статус на диспетчерской панели, создать оповещение о преследовании, видимое для всех подразделений, и начать GPS-трекинг автомобиля офицера. Когда они звонят 10-97 (прибывая на место), система диспетчеризации отмечает их как находящихся на месте для активного вызова, на который они реагируют. Постройте систему 10-кодов как настраиваемую таблицу, чтобы владельцы серверов могли адаптировать коды под предпочитаемый протокол их департамента:

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)

Очередь вызовов диспетчера

Очередь диспетчеризации , центральный узел, который обрабатывает все входящие экстренные вызовы и направляет их соответствующим подразделениям. Когда гражданин звонит 911, срабатывает автоматическая сигнализация в банке или магазине, или полицейский запрашивает подкрепление, система создаёт запись диспетчеризации с уровнем приоритета, местоположением, описанием и типом необходимой службы. Очередь отображает все активные вызовы, отсортированные по приоритету, на панели диспетчеризации NUI, показывая время вызова, местоположение, статус и назначенные подразделения. Выделенная роль диспетчера может вручную назначать вызовы конкретным подразделениям, или офицеры могут самостоятельно назначаться, кликая вызов и отмечая себя как отвечающих. Реализуйте автоматическое повышение приоритета вызовов, если они остаются без ответа после настраиваемого таймаута, гарантируя, что ни одна чрезвычайная ситуация не останется без внимания в периоды загруженности:

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)

GPS отслеживание и карта юнитов

GPS отслеживание обеспечивает видимость в реальном времени, где расположены все активные юниты на карте, позволяя диспетчерам и командирам принимать обоснованные решения о назначениях. Позиция каждого дежурного офицера передается в диспетчерскую систему с настраиваемым интервалом, обычно каждые 5-10 секунд, и отображается на общей панели карты NUI, доступной операторам диспетчерской и руководителям. На карте отображаются маркеры юнитов, окрашенные по отделам и статусу, с иконками, указывающими, доступны ли они, реагируют, на месте происшествия или в преследовании. Включена функция следа, которая отслеживает движение юнита за последние несколько минут, чтобы диспетчеры могли видеть направление движения во время преследований. Реализована визуализация радиуса вызова, которая рисует круг вокруг активных диспетчерских вызовов, показывая рекомендуемую зону реагирования. Когда диспетчеру нужно найти ближайший доступный юнит к новому вызову, система может вычислить расстояния от всех доступных юнитов и предложить оптимальное назначение. Поддерживайте эффективность передачи GPS, отправляя обновления позиции только когда юнит переместился более чем на минимальный порог расстояния, чтобы снизить сетевой трафик на загруженных серверах.

Система оповещений и кнопка паники

Система оповещений обрабатывает уведомления высокого приоритета, требующие немедленного внимания всех подразделений. Самое критичное оповещение , кнопка паники, активируемая, когда офицер вызывает помощь 10-78 или 10-99 , который отправляет свое GPS-местоположение всем дежурным подразделениям с мигающей меткой и характерным звуком тревоги. Сигнал тревоги должен иметь приоритет над обычной очередью диспетчеризации и отображаться на экране каждого офицера до подтверждения. Реализуйте разные уровни тревог с нарастающими реакциями: стандартная тревога создает вызов диспетчера с уведомлением, срочная тревога мигает панелью диспетчера и воспроизводит предупреждающий звук, критическая тревога вызывает полноэкранное уведомление с сиреной и автоматическим маршрутом GPS к месту тревоги. Автоматические тревоги интегрируются с другими системами сервера: зоны обнаружения выстрелов вызывают тревоги при стрельбе в общественных местах, камеры скорости отмечают автомобили, превышающие лимит, а системы сигнализации магазинов создают вызовы при начале ограбления. Для каждого типа тревог предусмотрены настраиваемые интервалы ожидания, чтобы предотвратить спам от повторных срабатываний в одной зоне.

Роль оператора диспетчера

Диспетчер , это выделенная роль, которая сидит на рабочем месте и управляет потоком экстренных вызовов к полевым подразделениям. В отличие от офицеров, которые видят упрощённое уведомление диспетчера, оператор имеет полный диспетчерский консоль NUI с менеджером очереди, списком подразделений, живой картой и средствами связи. Оператор может приоритизировать вызовы, перетаскивая их в очереди, назначать конкретные подразделения на вызовы по близости и доступности, отмечать вызовы как решённые, когда подразделения докладывают о завершении, и рассылать сообщения всем подразделениям на частоте департамента. Реализуйте систему рабочего места, где диспетчеры должны находиться за назначенным столом диспетчера для доступа к полной консоли, предотвращая работу с системой диспетчеризации в поле. Роль оператора должна иметь собственную прогрессию по должности, где опытные диспетчеры получают доступ к инструментам междепартаментской координации и возможностям экстренного вещания. Отслеживайте показатели работы диспетчера, такие как среднее время отклика, количество вызовов за смену и процент разрешённых вызовов, чтобы предоставлять операторам обратную связь и давать администраторам данные для принятия решений по персоналу.

Интеграция голосового радио

Для серверов, использующих голосовые чаты, такие как pma-voice или mumble-voip, интегрируйте радиосистему с голосом по близости для создания реалистичной радиосвязи. Когда игрок передаёт на радиоканале, удерживая клавишу, его голос слышат все игроки на той же частоте независимо от физической близости, с применением радиофильтра для отличия радиосвязи от разговора лицом к лицу. Реализуйте систему squelch, где игроки должны отпустить клавишу передачи, прежде чем другой сможет говорить, имитируя поведение радиостанции с полудуплексом. Добавьте звуковые эффекты нажатия и отпускания клавиши передачи для аудиофидбэка. Интеграция голоса должна учитывать права доступа к каналам, определённые в радиосистеме, чтобы игроки, не подключённые к частоте через радио NUI, не могли слышать или передавать, даже если знают ID mumble-канала. Для серверов без голосового чата используйте текстовую радиосистему, где сообщения на частоте отображаются в стилизованном чате с позывным отправителя и идентификатором канала.

Интеграция и расширяемость

Ваша радиосистема и система диспетчеризации должны предоставлять чистый API, который другие ресурсы могут использовать для создания вызовов диспетчера, отправки оповещений и запроса статуса подразделений без необходимости понимать внутреннюю реализацию. Экспортируйте функции, такие как CreateDispatchCall, SendAlert, GetAvailableUnits , и GetUnitStatus чтобы любой ресурс на сервере мог интегрироваться с системой диспетчеризации. Когда начинается ограбление банка, банковский ресурс вызывает экспорт диспетчера для создания вызова. Когда EMS получает звонок 911, ресурс больницы направляет его через ту же систему диспетчеризации. Такой централизованный подход гарантирует, что вся экстренная связь проходит через единую систему с согласованной обработкой приоритетов, отслеживанием подразделений и логированием. Храните историю вызовов диспетчера в базе данных для проверки администраторами и генерируйте отчеты по сменам, которые суммируют объем вызовов, время реакции и активность подразделений за каждый период дежурства. Подключите систему диспетчеризации к Discord webhook, чтобы критические оповещения, такие как вызовы о ранении офицера или ограбления банков, появлялись в выделенном канале для персонала, информируя администраторов сервера даже когда они не в игре.

Готовы начать?

Возьмите скрипты в нашем магазине или заходите в Discord за поддержкой, обновлениями и анонсами.