aether-framework/resources/[aether]/aether-police/client/main.lua
Jerrit Fritzsche e4b82dd685 Aether-Framework: Fachmodule, Admin-Panel und Docker-Datenbank
- 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>
2026-07-27 18:55:32 +02:00

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)