FiveM-Funk- und Einsatzleitsystem: PMA, pma-voice und CADs
Richte Funk-, Einsatzleit- und CAD-Systeme für FiveM ein. pma-voice-Kanäle, MDT-Tools und die besten Skripte für Polizei, Rettungsdienst und Mechaniker.
Agency Scripts
Gründer und leitender Entwickler bei Agency Scripts
Architektur für Funk- und Dispositionssysteme
Ein Funk- und Dispositionssystem bildet das Kommunikationsrückgrat von Rettungsdiensten auf einem FiveM-Rollenspielserver. Es ermöglicht Polizei, EMS und Feuerwehr, Einsätze über strukturierte Funkkanäle zu koordinieren, priorisierte Dispositionsmeldungen zu 911-Anrufen und automatischen Erkennungssystemen zu erhalten und den Standort der Einsatzkräfte über eine GPS-Integration in Echtzeit zu verfolgen. Die Architektur besteht aus drei miteinander verbundenen Systemen: dem Funkkanal-Framework, das verwaltet, wer auf welcher Frequenz sprechen darf, der Dispositionswarteschlange, die eingehende Anrufe verarbeitet und den passenden Einsatzkräften zuweist, und der GPS-Tracking-Ebene, die die Standorte der Einsatzkräfte auf einer gemeinsamen Karte visualisiert. Anders als bei einfacher chatbasierter Kommunikation vermittelt ein richtiges Funksystem das Gefühl, Frequenzen zu wechseln, Funkverkehr nur dann zu hören, wenn man den passenden Kanal eingestellt hat, und strukturierte Kommunikationsprotokolle zu befolgen, die denen realer Rettungsdienste ähneln. Das gesamte System läuft über serverseitiges Statusmanagement, um Cheating zu verhindern. Die clientseitige NUI stellt die Funkoberfläche und das Dispositionspanel bereit.
Funksystem mit Kanälen
Funkkanäle organisieren die Kommunikation, damit verschiedene Abteilungen und Einheiten ohne Überschneidungen miteinander kommunizieren können. Jeder Kanal hat eine Frequenznummer, Zugriffsbeschränkungen auf Basis der Job-Rollen und eine Liste der aktuell verbundenen Spieler. Das System unterstützt mehrere Kanaltypen: abteilungsweite Kanäle für die allgemeine Kommunikation, taktische Kanäle für bestimmte Einsätze wie eine Verfolgungsjagd oder einen Banküberfall sowie abteilungsübergreifende Kanäle, über die Polizei und EMS gemeinsame Einsätze koordinieren können. Spieler treten einem Kanal über ein Funkgerät-Item oder eine NUI-Oberfläche bei. Alle über diesen Kanal übertragenen Sprach- oder Textnachrichten erreichen nur Spieler, die dieselbe Frequenz eingestellt haben. Implementiere ein Prioritätssprechersystem, über das Disponenten und leitende Beamte Notfallmeldungen auf allen Kanälen gleichzeitig senden können:
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-Codesystem
Zehnercodes sind standardisierte Funkkürzel, die der Kommunikation von Rettungsdiensten Authentizität verleihen und schnelle Statusupdates über das Dispositionssystem ermöglichen. Statt 10-Codes lediglich als Chat-Makros zu implementieren, solltest du sie als funktionale Befehle umsetzen, die den Status des Beamten im Dispositionssystem aktualisieren und automatische Reaktionen auslösen. Wenn ein Beamter meldet 10-80 (Verfolgungsjagd läuft), sollte das System automatisch seinen Status im Dispositionsboard aktualisieren, eine Verfolgungsmeldung erstellen, die für alle Einheiten sichtbar ist, und das GPS-Tracking seines Fahrzeugs starten. Wenn er meldet 10-97 (am Einsatzort angekommen), markiert das Dispositionssystem ihn für den aktiven Einsatz, auf den er reagiert, als vor Ort. Implementiere das 10-Codesystem als konfigurierbare Tabelle, damit Serverbetreiber die Codes an das bevorzugte Protokoll ihrer Abteilung anpassen können:
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)
Warteschlange für Dispositionsmeldungen
Die Warteschlange für Dispositionsmeldungen ist die zentrale Stelle, die alle eingehenden Notrufe verarbeitet und den passenden Einsatzkräften zuweist. Wenn ein Zivilist 911 anruft, ein automatischer Alarm in einer Bank oder einem Geschäft ausgelöst wird oder ein Polizeibeamter Verstärkung anfordert, erstellt das System einen Dispositionseintrag mit Prioritätsstufe, Standort, Beschreibung und benötigter Dienstleistung. Die Warteschlange zeigt auf dem Dispositionspanel der NUI alle aktiven Einsätze nach Priorität sortiert an, einschließlich Anrufzeit, Standort, Status und zugewiesenen Einheiten. Eine eigene Disponentenrolle kann Einsätze manuell bestimmten Einheiten zuweisen. Alternativ können Beamte sich selbst zuweisen, indem sie auf einen Einsatz klicken und markieren, dass sie darauf reagieren. Implementiere eine automatische Eskalation von Einsätzen, bei der die Priorität unbeantworteter Anrufe nach einer konfigurierbaren Zeitüberschreitung steigt. So wird sichergestellt, dass in geschäftigen Phasen kein Notfall unbeachtet bleibt:
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-Tracking und Einheitenkarte
GPS-Tracking zeigt in Echtzeit, wo sich alle aktiven Einheiten auf der Karte befinden. So können Disponenten und leitende Beamte fundierte Zuweisungsentscheidungen treffen. Der Standort jedes diensthabenden Beamten wird in einem konfigurierbaren Intervall, üblicherweise alle 5-10 Sekunden, an das Dispositionssystem übertragen und auf einem gemeinsamen NUI-Kartenpanel angezeigt, auf das Disponenten und Vorgesetzte zugreifen können. Auf der Karte werden Einheiten-Markierungen nach Abteilung und Status farbcodiert angezeigt. Symbole geben an, ob sie verfügbar, unterwegs, am Einsatzort oder in einer Verfolgungsjagd sind. Füge eine Verlaufsspur hinzu, die die Bewegungen der Einheiten der letzten Minuten aufzeichnet, damit Disponenten während Verfolgungsjagden die Fahrtrichtung erkennen können. Implementiere eine Visualisierung des Einsatzradius, die einen Kreis um aktive Dispositionseinsätze zeichnet und den empfohlenen Einsatzbereich anzeigt. Wenn ein Disponent die nächste verfügbare Einheit für einen neuen Einsatz finden muss, kann das System die Entfernung aller verfügbaren Einheiten berechnen und eine optimale Zuweisung vorschlagen. Halte die GPS-Übertragung effizient, indem du Positionsupdates nur dann sendest, wenn sich eine Einheit um mehr als eine Mindestdistanz bewegt hat. So reduzierst du den Netzwerkverkehr auf stark ausgelasteten Servern.
Warnsystem und Panikknopf
Das Warnsystem verarbeitet wichtige Meldungen mit hoher Priorität, die sofortige Aufmerksamkeit aller Einheiten erfordern. Die wichtigste Meldung ist der Panikknopf. Wird er von einem Beamten betätigt, indem er 10-78 oder 10-99ruft, sendet das System seinen GPS-Standort mit einer blinkenden Markierung und einem charakteristischen Alarmton an alle diensthabenden Einheiten. Der Panikalarm sollte die normale Priorität der Dispositionswarteschlange übersteuern und auf dem Bildschirm jedes Beamten deutlich sichtbar bleiben, bis er bestätigt wurde. Implementiere verschiedene Warnstufen, die unterschiedlich starke Reaktionen auslösen: Eine normale Warnung erstellt mit einem Benachrichtigungston einen Dispositionseinsatz, eine dringende Warnung lässt das Dispositionspanel blinken und spielt einen Warnton ab, und eine kritische Warnung löst eine Vollbildbenachrichtigung mit Sirenenton und automatischer GPS-Navigation zum Warnungsstandort aus. Automatische Warnungen lassen sich mit anderen Serversystemen verknüpfen, sodass Schussdetektionszonen bei abgefeuerten Waffen in öffentlichen Bereichen Warnungen auslösen, Geschwindigkeitskameras Fahrzeuge erfassen, die das Tempolimit überschreiten, und Ladensicherungssysteme bei beginnenden Raubüberfällen Dispositionseinsätze erstellen. Für jeden Warnungstyp lassen sich Abklingzeiten konfigurieren, um Spam durch wiederholte Auslösungen im selben Gebiet zu verhindern.
Rolle des Disponenten
Der Disponent ist eine eigene Rolle, die an einem Arbeitsplatz sitzt und den Eingang von Notrufen an Einsatzkräfte koordiniert. Anders als Beamte, die vereinfachte Dispositionsmeldungen sehen, hat der Disponent Zugriff auf eine vollständige Dispositionskonsole mit einem Warteschlangen-Manager, einer Einheitenübersicht, einer Live-Karte und Kommunikationstools. Er kann Einsätze durch Verschieben in der Warteschlange priorisieren, bestimmten Einheiten basierend auf Nähe und Verfügbarkeit Einsätze zuweisen, Einsätze als erledigt markieren, wenn Einheiten ihren Status als frei melden, und Nachrichten an alle Einheiten auf einer Abteilungsfrequenz senden. Implementiere ein Arbeitsplatzsystem, bei dem Disponenten an einem dafür vorgesehenen Dispositionstisch sitzen müssen, um auf die vollständige Konsole zuzugreifen. So können sie das Dispositionssystem nicht im Außeneinsatz bedienen. Die Disponentenrolle sollte über eine eigene Job-Laufbahn mit Beförderungsstufen verfügen, durch die erfahrene Disponenten Zugriff auf Tools zur abteilungsübergreifenden Koordination und Notfalldurchsagen erhalten. Erfasse Leistungskennzahlen wie die durchschnittliche Reaktionszeit, die Anzahl bearbeiteter Einsätze pro Schicht und die Erfolgsquote bei der Einsatzlösung. So erhalten Disponenten Feedback zu ihrer Leistung und Serveradministratoren Daten für Personalentscheidungen.
Integration von Sprachfunk
Integriere das Funksystem für Server, die Sprachchat-Lösungen wie pma-voice oder mumble-voip verwenden, mit dem Näherungs-Sprachchat, um realistische Funkkommunikation zu ermöglichen. Wenn ein Spieler über eine Taste auf einem Funkkanal sendet, sollte seine Stimme unabhängig von der räumlichen Nähe von allen Spielern auf derselben Frequenz gehört werden. Ein Funkfiltereffekt sollte die Funkkommunikation von Gesprächen im direkten Umfeld unterscheiden. Implementiere ein Squelch-System, bei dem Spieler die Sprechtaste loslassen müssen, bevor ein anderer Spieler sprechen kann, um das Halbduplex-Verhalten echter Funkgeräte nachzuahmen. Füge beim Starten und Beenden der Übertragung Funkgeräusche hinzu, damit akustisch erkennbar ist, wenn jemand zu sprechen beginnt oder aufhört. Die Sprachintegration muss die im Funksystem festgelegten Zugriffskontrollen beachten, damit Spieler, die einer Frequenz nicht über die NUI des Funkgeräts beigetreten sind, sie weder hören noch darauf senden können, selbst wenn sie irgendwie die mumble-Kanal-ID kennen. Für Server ohne Sprachchat kannst du auf ein textbasiertes Funksystem zurückgreifen, bei dem Nachrichten auf einer Frequenz in einem gestalteten Chatformat mit Rufzeichen des Absenders und Kanalkennung angezeigt werden.
Integration und Erweiterbarkeit
Dein Funk- und Dispositionssystem sollte eine übersichtliche API bereitstellen, die andere Resources zum Erstellen von Dispositionseinsätzen, Senden von Warnungen und Abfragen des Einheitenstatus verwenden können, ohne die interne Implementierung kennen zu müssen. Stelle Export-Funktionen bereit, etwa CreateDispatchCall, SendAlert, GetAvailableUnits , und GetUnitStatus damit jede Ressource auf dem Server in das Dispatch-System integriert werden kann. Wenn ein Banküberfall beginnt, ruft die Banking-Ressource den Dispatch-Export auf, um einen Einsatz zu erstellen. Wenn EMS einen 911-Notruf erhält, leitet die Krankenhaus-Ressource ihn über dasselbe Dispatch-System weiter. Dieser zentrale Ansatz stellt sicher, dass die gesamte Notfallkommunikation über ein einziges System mit einheitlicher Prioritätsverwaltung, Einheitenverfolgung und Protokollierung läuft. Speichere den Verlauf der Dispatch-Einsätze in der Datenbank, damit Admins ihn überprüfen können, und erstelle Schichtberichte, die das Einsatzaufkommen, die Reaktionszeiten und die Aktivitäten der Einheiten für jeden Dienstzeitraum zusammenfassen. Verbinde das Dispatch-System mit Discord-Webhooks, damit wichtige Meldungen wie Notrufe zu niedergeschossenen Beamten oder Banküberfälle in einem eigenen Teamkanal angezeigt werden. So bleiben die Server-Admins informiert, auch wenn sie nicht im Spiel sind.