- aether-medical: Stationen/Belegung, Intensivstation, Blutbank, Fuhrpark, Nachrichten/Aushänge, Berichte und Textbausteine (server + NUI) - aether-admin: vollständiges Server-Admin-Panel als echtes FiveM-Resource, an aether-core gekoppelt (Rechte über aether_users.admin_group), mit WebRTC-Live-Kameras (Bildschirmfreigabe) und separatem Media-Server (SFU) - Prototypen admin/ vervollständigt - SQL-Schema aether_admin.sql - Docker-Compose mit MariaDB und idempotenter Auto-Migration (schema_migrations) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
377 lines
13 KiB
Lua
377 lines
13 KiB
Lua
-- =====================================================================
|
|
-- aether-police — Clientlogik
|
|
-- Öffnet das Tablet und vermittelt zwischen Oberfläche und Server.
|
|
-- =====================================================================
|
|
|
|
local istOffen = false
|
|
local istGeladen = false
|
|
local imDienst = false
|
|
|
|
local function SendeAnNui(nachricht)
|
|
SendNuiMessage(json.encode(nachricht))
|
|
end
|
|
|
|
local function ZeigeSpielmeldung(text)
|
|
if not text or text == '' then return end
|
|
BeginTextCommandThefeedPost('STRING')
|
|
AddTextComponentSubstringPlayerName(text)
|
|
EndTextCommandThefeedPostTicker(false, true)
|
|
end
|
|
|
|
local function Schliessen()
|
|
if not istOffen then return end
|
|
istOffen = false
|
|
SetNuiFocus(false, false)
|
|
SendeAnNui({ aktion = 'schliessen' })
|
|
pcall(function() exports['aether-hud']:SetzeSichtbar(true) end)
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Ereignisse vom Server
|
|
-- ---------------------------------------------------------------------
|
|
AddEventHandler('aether:client:bereit', function()
|
|
istGeladen = true
|
|
end)
|
|
|
|
--- Tablet mit vollem Zustand anzeigen
|
|
RegisterNetEvent('aether:police:zeige', function(daten)
|
|
istOffen = true
|
|
imDienst = true
|
|
SetNuiFocus(true, true)
|
|
pcall(function() exports['aether-hud']:SetzeSichtbar(false) end)
|
|
daten.aktion = 'oeffnen'
|
|
SendeAnNui(daten)
|
|
end)
|
|
|
|
--- Anmeldebildschirm (noch nicht im Dienst)
|
|
RegisterNetEvent('aether:police:anmeldung', function(daten)
|
|
istOffen = true
|
|
SetNuiFocus(true, true)
|
|
pcall(function() exports['aether-hud']:SetzeSichtbar(false) end)
|
|
daten.aktion = 'anmeldung'
|
|
SendeAnNui(daten)
|
|
end)
|
|
|
|
RegisterNetEvent('aether:police:schliessen', function()
|
|
imDienst = false
|
|
Schliessen()
|
|
ZeigeSpielmeldung('Dienst beendet.')
|
|
end)
|
|
|
|
--- Antwort auf einen Modul-Aufruf
|
|
RegisterNetEvent('aether:police:antwort', function(ruf, ergebnis, fehler)
|
|
SendeAnNui({ aktion = 'antwort', ruf = ruf, ergebnis = ergebnis, fehler = fehler })
|
|
end)
|
|
|
|
--- Dienstliste (Beamte im Dienst, für Dashboard und Karte)
|
|
RegisterNetEvent('aether:police:dienstliste', function(liste)
|
|
if istOffen then
|
|
SendeAnNui({ aktion = 'dienstliste', beamte = liste })
|
|
end
|
|
end)
|
|
|
|
--- Hinweis / Erinnerung
|
|
RegisterNetEvent('aether:police:hinweis', function(hinweis)
|
|
if istOffen then
|
|
SendeAnNui({ aktion = 'hinweis', hinweis = hinweis })
|
|
else
|
|
-- Auch ausserhalb des Tablets erreichbar, damit Fristen nicht untergehen
|
|
local praefix = hinweis.prioritaet == 'kritisch' and '~r~' or
|
|
hinweis.prioritaet == 'hoch' and '~o~' or '~b~'
|
|
ZeigeSpielmeldung(praefix .. hinweis.titel .. '~s~' ..
|
|
(hinweis.text and ('\n' .. hinweis.text) or ''))
|
|
end
|
|
end)
|
|
|
|
RegisterNetEvent('aether:police:fehler', function(text)
|
|
if istOffen then
|
|
SendeAnNui({ aktion = 'fehler', text = text })
|
|
else
|
|
ZeigeSpielmeldung(text)
|
|
end
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Fahrzeug-Telemetrie
|
|
--
|
|
-- Meldet Tatsachen über das gefahrene Dienstfahrzeug: Position,
|
|
-- Tankstand, Zustand. Der Server verwirft die Meldung, wenn das
|
|
-- Kennzeichen nicht zum Fuhrpark gehört.
|
|
-- ---------------------------------------------------------------------
|
|
CreateThread(function()
|
|
while true do
|
|
Wait(30000) -- alle 30 Sekunden genügt
|
|
|
|
if imDienst then
|
|
local ped = PlayerPedId()
|
|
if IsPedInAnyVehicle(ped, false) then
|
|
local fahrzeug = GetVehiclePedIsIn(ped, false)
|
|
-- Nur melden, wenn man selbst am Steuer sitzt
|
|
if GetPedInVehicleSeat(fahrzeug, -1) == ped then
|
|
local position = GetEntityCoords(fahrzeug)
|
|
TriggerServerEvent('aether:police:fahrzeugMelden', {
|
|
kennzeichen = GetVehicleNumberPlateText(fahrzeug),
|
|
tank = GetVehicleFuelLevel(fahrzeug),
|
|
zustand = GetVehicleBodyHealth(fahrzeug),
|
|
x = position.x, y = position.y, z = position.z,
|
|
})
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Gewahrsam
|
|
-- Der Client führt nur aus, was ein Beamter entschieden hat.
|
|
-- ---------------------------------------------------------------------
|
|
|
|
--- In eine Zelle verbracht werden
|
|
RegisterNetEvent('aether:police:gewahrsamVerbringen', function(daten)
|
|
Schliessen()
|
|
DoScreenFadeOut(600)
|
|
Wait(800)
|
|
|
|
local ped = PlayerPedId()
|
|
SetEntityCoordsNoOffset(ped, daten.x + 0.0, daten.y + 0.0, daten.z + 0.0, false, false, false)
|
|
SetEntityHeading(ped, daten.w or 0.0)
|
|
|
|
-- Warten, bis die Umgebung geladen ist
|
|
local versuche = 0
|
|
while not HasCollisionLoadedAroundEntity(ped) and versuche < 60 do
|
|
Wait(50)
|
|
versuche = versuche + 1
|
|
end
|
|
|
|
DoScreenFadeIn(800)
|
|
|
|
if daten.rest then
|
|
ZeigeSpielmeldung(('~y~Gewahrsam~s~ · %s\n%s\nRestzeit laut Akte: %d Minuten')
|
|
:format(daten.zelle or '', daten.grund or '', math.max(0, daten.rest)))
|
|
else
|
|
ZeigeSpielmeldung(('~y~Du wurdest in Gewahrsam genommen~s~ · %s'):format(daten.zelle or ''))
|
|
end
|
|
end)
|
|
|
|
--- Entlassung durch einen Beamten
|
|
RegisterNetEvent('aether:police:gewahrsamEntlassen', function(daten)
|
|
DoScreenFadeOut(600)
|
|
Wait(800)
|
|
|
|
local ped = PlayerPedId()
|
|
SetEntityCoordsNoOffset(ped, daten.x + 0.0, daten.y + 0.0, daten.z + 0.0, false, false, false)
|
|
SetEntityHeading(ped, daten.w or 0.0)
|
|
|
|
local versuche = 0
|
|
while not HasCollisionLoadedAroundEntity(ped) and versuche < 60 do
|
|
Wait(50)
|
|
versuche = versuche + 1
|
|
end
|
|
|
|
DoScreenFadeIn(800)
|
|
ZeigeSpielmeldung('~g~Du wurdest aus dem Gewahrsam entlassen.~s~')
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Terminals an den Wachen
|
|
-- ---------------------------------------------------------------------
|
|
CreateThread(function()
|
|
for _, terminal in ipairs(Polizei.Config.Terminals) do
|
|
local blip = AddBlipForCoord(terminal.pos.x, terminal.pos.y, terminal.pos.z)
|
|
SetBlipSprite(blip, 60)
|
|
SetBlipColour(blip, 3)
|
|
SetBlipScale(blip, 0.8)
|
|
SetBlipAsShortRange(blip, true)
|
|
BeginTextCommandSetBlipName('STRING')
|
|
AddTextComponentString('Polizeiwache ' .. terminal.name)
|
|
EndTextCommandSetBlipName(blip)
|
|
end
|
|
|
|
while true do
|
|
local pause = 800
|
|
|
|
if istGeladen and not istOffen then
|
|
local position = GetEntityCoords(PlayerPedId())
|
|
|
|
for _, terminal in ipairs(Polizei.Config.Terminals) do
|
|
if #(position - terminal.pos) <= Polizei.Config.TerminalWeite then
|
|
pause = 0
|
|
SetTextFont(4)
|
|
SetTextScale(0.34, 0.34)
|
|
SetTextColour(255, 255, 255, 215)
|
|
SetTextCentre(true)
|
|
SetTextOutline()
|
|
SetTextEntry('STRING')
|
|
AddTextComponentString('[E] Terminal ' .. terminal.name)
|
|
SetDrawOrigin(terminal.pos.x, terminal.pos.y, terminal.pos.z + 0.9, 0)
|
|
DrawText(0.0, 0.0)
|
|
ClearDrawOrigin()
|
|
|
|
if IsControlJustReleased(0, 38) then
|
|
TriggerServerEvent('aether:police:oeffnen')
|
|
end
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
Wait(pause)
|
|
end
|
|
end)
|
|
|
|
--- Tablet jederzeit öffnen, sobald man im Dienst ist
|
|
RegisterCommand('mdt', function()
|
|
if istGeladen and not istOffen then
|
|
TriggerServerEvent('aether:police:oeffnen')
|
|
end
|
|
end, false)
|
|
RegisterKeyMapping('mdt', 'Polizei-Tablet öffnen', 'keyboard', 'F5')
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Panikknopf (10-13)
|
|
-- ---------------------------------------------------------------------
|
|
local panikGesperrtBis = 0
|
|
|
|
RegisterCommand('panik', function()
|
|
if not imDienst then return end
|
|
|
|
-- Kurze Sperre, damit der Alarm nicht versehentlich mehrfach ausgeht
|
|
if GetGameTimer() < panikGesperrtBis then return end
|
|
panikGesperrtBis = GetGameTimer() + 10000
|
|
|
|
TriggerServerEvent('aether:police:panik')
|
|
ZeigeSpielmeldung('~r~10-13 ausgelöst~s~ — Verstärkung wird alarmiert.')
|
|
end, false)
|
|
RegisterKeyMapping('panik', 'Panikknopf (10-13)', 'keyboard', 'F7')
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Einsatzmarkierungen auf der Karte
|
|
-- ---------------------------------------------------------------------
|
|
local einsatzBlips = {}
|
|
|
|
--- Entfernt alle Einsatzmarkierungen
|
|
local function LeereEinsatzBlips()
|
|
for id, blip in pairs(einsatzBlips) do
|
|
RemoveBlip(blip)
|
|
einsatzBlips[id] = nil
|
|
end
|
|
end
|
|
|
|
--- Setzt Markierungen für die übergebenen Einsätze
|
|
local function SetzeEinsatzBlips(einsaetze)
|
|
LeereEinsatzBlips()
|
|
|
|
for _, einsatz in ipairs(einsaetze or {}) do
|
|
if einsatz.pos_x then
|
|
local blip = AddBlipForCoord(einsatz.pos_x + 0.0, einsatz.pos_y + 0.0, einsatz.pos_z + 0.0)
|
|
SetBlipSprite(blip, 161)
|
|
-- Offene Einsätze fallen durch Blinken auf
|
|
SetBlipColour(blip, einsatz.status == 'offen' and 1 or 5)
|
|
SetBlipScale(blip, einsatz.prioritaet == 1 and 1.0 or 0.8)
|
|
SetBlipFlashes(blip, einsatz.status == 'offen' and einsatz.prioritaet <= 2)
|
|
BeginTextCommandSetBlipName('STRING')
|
|
AddTextComponentString(einsatz.nummer .. ' · ' .. einsatz.titel)
|
|
EndTextCommandSetBlipName(blip)
|
|
einsatzBlips[einsatz.id] = blip
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Es hat sich etwas an den Einsätzen geändert
|
|
RegisterNetEvent('aether:police:einsatzAenderung', function()
|
|
if istOffen then
|
|
SendeAnNui({ aktion = 'einsatzAenderung' })
|
|
end
|
|
end)
|
|
|
|
--- Panikalarm einer anderen Einheit
|
|
RegisterNetEvent('aether:police:panikAlarm', function(daten)
|
|
PlaySoundFrontend(-1, 'Lose_1st', 'GTAO_FM_Events_Soundset', true)
|
|
ZeigeSpielmeldung(('~r~10-13~s~ — %s (%s) benötigt Verstärkung!')
|
|
:format(daten.name, daten.dienstnummer))
|
|
|
|
if daten.position then
|
|
SetNewWaypoint(daten.position.x, daten.position.y)
|
|
end
|
|
|
|
if istOffen then
|
|
SendeAnNui({ aktion = 'panikAlarm', daten = daten })
|
|
end
|
|
end)
|
|
|
|
--- Die Leitstelle bittet um Übernahme eines Einsatzes
|
|
RegisterNetEvent('aether:police:einsatzZuweisung', function(daten)
|
|
PlaySoundFrontend(-1, 'Event_Start_Text', 'GTAO_FM_Events_Soundset', true)
|
|
|
|
if daten.position then
|
|
SetNewWaypoint(daten.position.x, daten.position.y)
|
|
end
|
|
|
|
if istOffen then
|
|
SendeAnNui({ aktion = 'einsatzZuweisung', daten = daten })
|
|
else
|
|
ZeigeSpielmeldung(('~b~Zuweisung von %s~s~\n%s — %s')
|
|
:format(daten.von, daten.nummer, daten.titel))
|
|
end
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- NUI-Callbacks
|
|
-- ---------------------------------------------------------------------
|
|
RegisterNUICallback('schliessen', function(_, rueckgabe)
|
|
Schliessen()
|
|
rueckgabe({ ok = true })
|
|
end)
|
|
|
|
--- Zentraler Modul-Aufruf: die Oberfläche fragt, der Server antwortet
|
|
RegisterNUICallback('aufruf', function(daten, rueckgabe)
|
|
TriggerServerEvent('aether:police:aufruf',
|
|
tostring(daten.modul or ''), tostring(daten.aktion or ''),
|
|
daten.daten or {}, daten.ruf)
|
|
rueckgabe({ ok = true })
|
|
end)
|
|
|
|
RegisterNUICallback('dienstAntreten', function(_, rueckgabe)
|
|
TriggerServerEvent('aether:police:dienstAntreten')
|
|
rueckgabe({ ok = true })
|
|
end)
|
|
|
|
RegisterNUICallback('dienstBeenden', function(_, rueckgabe)
|
|
TriggerServerEvent('aether:police:dienstBeenden')
|
|
rueckgabe({ ok = true })
|
|
end)
|
|
|
|
RegisterNUICallback('status', function(daten, rueckgabe)
|
|
TriggerServerEvent('aether:police:status', tostring(daten.status or ''))
|
|
rueckgabe({ ok = true })
|
|
end)
|
|
|
|
RegisterNUICallback('leitstelle', function(daten, rueckgabe)
|
|
TriggerServerEvent('aether:police:leitstelle', tonumber(daten.quelle))
|
|
rueckgabe({ ok = true })
|
|
end)
|
|
|
|
--- Einsatzmarkierungen auf der Karte auffrischen
|
|
RegisterNUICallback('einsatzBlips', function(daten, rueckgabe)
|
|
SetzeEinsatzBlips(daten.einsaetze or {})
|
|
rueckgabe({ ok = true })
|
|
end)
|
|
|
|
--- Wegpunkt auf der Karte setzen (z. B. zu einem Einsatzort)
|
|
RegisterNUICallback('wegpunkt', function(daten, rueckgabe)
|
|
if daten.x and daten.y then
|
|
SetNewWaypoint(daten.x + 0.0, daten.y + 0.0)
|
|
ZeigeSpielmeldung('Wegpunkt gesetzt.')
|
|
end
|
|
rueckgabe({ ok = true })
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Aufräumen
|
|
-- ---------------------------------------------------------------------
|
|
AddEventHandler('onResourceStop', function(ressource)
|
|
if ressource ~= GetCurrentResourceName() then return end
|
|
SetNuiFocus(false, false)
|
|
LeereEinsatzBlips()
|
|
end)
|