- 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>
331 lines
12 KiB
Lua
331 lines
12 KiB
Lua
-- =====================================================================
|
|
-- Aether Garage — Clientlogik
|
|
-- Erzeugt Fahrzeuge erst nach Freigabe durch den Server und meldet
|
|
-- beim Einparken den tatsaechlichen Zustand zurueck.
|
|
-- =====================================================================
|
|
|
|
local istOffen = false
|
|
local istGeladen = false
|
|
local aktuelleGarage = nil -- Garage, an der der Spieler gerade steht
|
|
|
|
-- Taste E
|
|
local TASTE_BENUTZEN = 38
|
|
|
|
--- Schickt eine Nachricht an die Oberflaeche
|
|
local function SendeAnNui(nachricht)
|
|
SendNuiMessage(json.encode(nachricht))
|
|
end
|
|
|
|
--- Zeigt eine Meldung oben links im Spiel
|
|
local function ZeigeSpielmeldung(text)
|
|
if not text or text == '' then return end
|
|
BeginTextCommandThefeedPost('STRING')
|
|
AddTextComponentSubstringPlayerName(text)
|
|
EndTextCommandThefeedPostTicker(false, true)
|
|
end
|
|
|
|
--- Schliesst die Garage
|
|
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
|
|
|
|
--- Wandelt eine Hex-Farbe in RGB-Werte um
|
|
--- @param hex string z. B. '#3563a8'
|
|
--- @return number, number, number
|
|
local function HexZuRgb(hex)
|
|
hex = (hex or '#20232a'):gsub('#', '')
|
|
return tonumber(hex:sub(1, 2), 16) or 32,
|
|
tonumber(hex:sub(3, 4), 16) or 35,
|
|
tonumber(hex:sub(5, 6), 16) or 42
|
|
end
|
|
|
|
--- Sucht ein Fahrzeug mit einem bestimmten Kennzeichen in der Naehe
|
|
--- @param kennzeichen string Kennzeichen
|
|
--- @return number|nil Fahrzeug-Handle
|
|
local function FindeFahrzeug(kennzeichen)
|
|
local position = GetEntityCoords(PlayerPedId())
|
|
for fahrzeug in EnumerateVehicles() do
|
|
if DoesEntityExist(fahrzeug) then
|
|
local schild = GetVehicleNumberPlateText(fahrzeug)
|
|
if schild and schild:gsub('%s', '') == kennzeichen:gsub('%s', '') then
|
|
if #(position - GetEntityCoords(fahrzeug)) < 30.0 then
|
|
return fahrzeug
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
--- Durchlaeuft alle Fahrzeuge in der Naehe
|
|
function EnumerateVehicles()
|
|
return coroutine.wrap(function()
|
|
local handle, fahrzeug = FindFirstVehicle()
|
|
local erfolgreich
|
|
repeat
|
|
coroutine.yield(fahrzeug)
|
|
erfolgreich, fahrzeug = FindNextVehicle(handle)
|
|
until not erfolgreich
|
|
EndFindVehicle(handle)
|
|
end)
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Karten-Markierungen
|
|
-- ---------------------------------------------------------------------
|
|
CreateThread(function()
|
|
for _, garage in pairs(Garage.Garagen) do
|
|
if garage.typ == 'oeffentlich' then
|
|
local blip = AddBlipForCoord(garage.position.x, garage.position.y, garage.position.z)
|
|
SetBlipSprite(blip, 357)
|
|
SetBlipColour(blip, 3)
|
|
SetBlipScale(blip, 0.75)
|
|
SetBlipAsShortRange(blip, true)
|
|
BeginTextCommandSetBlipName('STRING')
|
|
AddTextComponentString('Garage ' .. garage.name)
|
|
EndTextCommandSetBlipName(blip)
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Ereignisse
|
|
-- ---------------------------------------------------------------------
|
|
AddEventHandler('aether:client:bereit', function()
|
|
istGeladen = true
|
|
end)
|
|
|
|
RegisterNetEvent('aether:garage:zeige', function(zustand)
|
|
istOffen = true
|
|
SetNuiFocus(true, true)
|
|
pcall(function() exports['aether-hud']:SetzeSichtbar(false) end)
|
|
zustand.aktion = 'oeffnen'
|
|
SendeAnNui(zustand)
|
|
end)
|
|
|
|
RegisterNetEvent('aether:garage:zustand', function(zustand)
|
|
if not istOffen then
|
|
ZeigeSpielmeldung(zustand.hinweis)
|
|
return
|
|
end
|
|
zustand.aktion = 'zustand'
|
|
SendeAnNui(zustand)
|
|
end)
|
|
|
|
RegisterNetEvent('aether:garage:fehler', function(text)
|
|
if istOffen then
|
|
SendeAnNui({ aktion = 'fehler', text = text })
|
|
else
|
|
ZeigeSpielmeldung(text)
|
|
end
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Eigenschaften anwenden, die nur clientseitig gesetzt werden koennen
|
|
-- Der Server erzeugt das Fahrzeug und legt die Werte im State Bag ab;
|
|
-- hier werden sie uebernommen, sobald das Fahrzeug beim Spieler auftaucht.
|
|
-- ---------------------------------------------------------------------
|
|
AddStateBagChangeHandler('aether', nil, function(bagName, _, werte)
|
|
if type(werte) ~= 'table' then return end
|
|
|
|
local netId = tonumber(bagName:match('entity:(%d+)'))
|
|
if not netId then return end
|
|
|
|
CreateThread(function()
|
|
-- Warten, bis die Entitaet lokal existiert
|
|
local versuche = 0
|
|
while not NetworkDoesEntityExistWithNetworkId(netId) and versuche < 100 do
|
|
Wait(50)
|
|
versuche = versuche + 1
|
|
end
|
|
if not NetworkDoesEntityExistWithNetworkId(netId) then return end
|
|
|
|
local fahrzeug = NetToVeh(netId)
|
|
if not fahrzeug or fahrzeug == 0 or not DoesEntityExist(fahrzeug) then return end
|
|
|
|
if werte.kennzeichen then
|
|
SetVehicleNumberPlateText(fahrzeug, werte.kennzeichen)
|
|
end
|
|
if werte.farbe then
|
|
local r, g, b = HexZuRgb(werte.farbe)
|
|
SetVehicleCustomPrimaryColour(fahrzeug, r, g, b)
|
|
SetVehicleCustomSecondaryColour(fahrzeug, r, g, b)
|
|
end
|
|
if werte.tank then SetVehicleFuelLevel(fahrzeug, werte.tank + 0.0) end
|
|
if werte.motor then SetVehicleEngineHealth(fahrzeug, werte.motor + 0.0) end
|
|
if werte.karosserie then SetVehicleBodyHealth(fahrzeug, werte.karosserie + 0.0) end
|
|
end)
|
|
end)
|
|
|
|
--- Server hat das Fahrzeug erzeugt: einsteigen
|
|
RegisterNetEvent('aether:garage:einsteigen', function(netId, kennzeichen)
|
|
Schliessen()
|
|
|
|
-- Warten, bis das Fahrzeug beim Spieler angekommen ist
|
|
local versuche = 0
|
|
while not NetworkDoesEntityExistWithNetworkId(netId) and versuche < 100 do
|
|
Wait(50)
|
|
versuche = versuche + 1
|
|
end
|
|
if not NetworkDoesEntityExistWithNetworkId(netId) then
|
|
ZeigeSpielmeldung('Fahrzeug konnte nicht geladen werden.')
|
|
return
|
|
end
|
|
|
|
local fahrzeug = NetToVeh(netId)
|
|
if fahrzeug and DoesEntityExist(fahrzeug) then
|
|
SetVehicleEngineOn(fahrzeug, true, true, false)
|
|
SetPedIntoVehicle(PlayerPedId(), fahrzeug, -1)
|
|
ZeigeSpielmeldung('Fahrzeug ausgeparkt · ' .. kennzeichen)
|
|
end
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Kilometerzaehler
|
|
-- GTA fuehrt keinen Tachostand, deshalb zaehlen wir die gefahrene
|
|
-- Strecke je Kennzeichen selbst mit (in Metern).
|
|
-- ---------------------------------------------------------------------
|
|
local gefahreneMeter = {} -- [kennzeichen] = Meter seit dem Ausparken
|
|
local letztePosition = nil -- Position im letzten Durchlauf
|
|
local seitMeldung = 0 -- Sekunden seit der letzten Zustandsmeldung
|
|
|
|
CreateThread(function()
|
|
while true do
|
|
Wait(1000)
|
|
|
|
local ped = PlayerPedId()
|
|
if istGeladen and IsPedInAnyVehicle(ped, false) then
|
|
local fahrzeug = GetVehiclePedIsIn(ped, false)
|
|
local position = GetEntityCoords(fahrzeug)
|
|
local kennzeichen = (GetVehicleNumberPlateText(fahrzeug) or ''):gsub('%s', '')
|
|
|
|
if letztePosition and kennzeichen ~= '' then
|
|
local strecke = #(position - letztePosition)
|
|
-- Sprünge durch Teleport oder Nachladen ignorieren
|
|
if strecke < 200.0 then
|
|
gefahreneMeter[kennzeichen] = (gefahreneMeter[kennzeichen] or 0) + strecke
|
|
end
|
|
end
|
|
|
|
-- Zustand regelmaessig melden, damit er auch bei einem
|
|
-- unerwarteten Serverstopp nicht verloren geht
|
|
seitMeldung = seitMeldung + 1
|
|
if seitMeldung >= 30 and kennzeichen ~= '' then
|
|
seitMeldung = 0
|
|
TriggerServerEvent('aether:garage:zustandMelden', kennzeichen, {
|
|
tank = GetVehicleFuelLevel(fahrzeug),
|
|
motor = GetVehicleEngineHealth(fahrzeug),
|
|
karosserie = GetVehicleBodyHealth(fahrzeug),
|
|
})
|
|
end
|
|
|
|
letztePosition = position
|
|
else
|
|
letztePosition = nil
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Garage in der Naehe erkennen
|
|
-- ---------------------------------------------------------------------
|
|
CreateThread(function()
|
|
while true do
|
|
local pause = 800
|
|
aktuelleGarage = nil
|
|
|
|
if istGeladen and not istOffen then
|
|
local position = GetEntityCoords(PlayerPedId())
|
|
|
|
for garageId, garage in pairs(Garage.Garagen) do
|
|
if #(position - garage.position) <= Garage.INTERAKTION_WEITE then
|
|
aktuelleGarage = garageId
|
|
pause = 0
|
|
|
|
SetTextFont(4)
|
|
SetTextScale(0.34, 0.34)
|
|
SetTextColour(255, 255, 255, 215)
|
|
SetTextCentre(true)
|
|
SetTextOutline()
|
|
SetTextEntry('STRING')
|
|
AddTextComponentString('[E] Garage ' .. garage.name)
|
|
SetDrawOrigin(garage.position.x, garage.position.y, garage.position.z + 0.9, 0)
|
|
DrawText(0.0, 0.0)
|
|
ClearDrawOrigin()
|
|
|
|
if IsControlJustReleased(0, TASTE_BENUTZEN) then
|
|
TriggerServerEvent('aether:garage:oeffnen', garageId)
|
|
end
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
Wait(pause)
|
|
end
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- NUI-Callbacks
|
|
-- ---------------------------------------------------------------------
|
|
RegisterNUICallback('schliessen', function(_, rueckgabe)
|
|
Schliessen()
|
|
rueckgabe({ ok = true })
|
|
end)
|
|
|
|
RegisterNUICallback('ausparken', function(daten, rueckgabe)
|
|
TriggerServerEvent('aether:garage:ausparken', daten.kennzeichen, daten.standort)
|
|
rueckgabe({ ok = true })
|
|
end)
|
|
|
|
RegisterNUICallback('einparken', function(daten, rueckgabe)
|
|
-- Tatsaechlichen Zustand des Fahrzeugs ermitteln und mitschicken
|
|
local fahrzeug = FindeFahrzeug(daten.kennzeichen)
|
|
local zustand = {}
|
|
|
|
if fahrzeug and DoesEntityExist(fahrzeug) then
|
|
local schluessel = (daten.kennzeichen or ''):gsub('%s', '')
|
|
zustand.tank = GetVehicleFuelLevel(fahrzeug)
|
|
zustand.motor = GetVehicleEngineHealth(fahrzeug)
|
|
zustand.karosserie = GetVehicleBodyHealth(fahrzeug)
|
|
zustand.meter = math.floor(gefahreneMeter[schluessel] or 0)
|
|
gefahreneMeter[schluessel] = 0 -- Zaehler nach dem Einparken zuruecksetzen
|
|
else
|
|
SendeAnNui({ aktion = 'fehler', text = 'Das Fahrzeug ist nicht in der Nähe.' })
|
|
rueckgabe({ ok = false })
|
|
return
|
|
end
|
|
|
|
TriggerServerEvent('aether:garage:einparken', daten.kennzeichen, daten.standort, zustand)
|
|
rueckgabe({ ok = true })
|
|
end)
|
|
|
|
RegisterNUICallback('ausloesen', function(daten, rueckgabe)
|
|
TriggerServerEvent('aether:garage:ausloesen', daten.kennzeichen, daten.standort)
|
|
rueckgabe({ ok = true })
|
|
end)
|
|
|
|
RegisterNUICallback('orten', function(daten, rueckgabe)
|
|
local fahrzeug = FindeFahrzeug(daten.kennzeichen)
|
|
if fahrzeug and DoesEntityExist(fahrzeug) then
|
|
local position = GetEntityCoords(fahrzeug)
|
|
SetNewWaypoint(position.x, position.y)
|
|
SendeAnNui({ aktion = 'hinweis', text = 'Markierung auf der Karte gesetzt.' })
|
|
else
|
|
SendeAnNui({ aktion = 'fehler', text = 'Das Fahrzeug ist nicht in der Nähe.' })
|
|
end
|
|
rueckgabe({ ok = true })
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Aufraeumen
|
|
-- ---------------------------------------------------------------------
|
|
AddEventHandler('onResourceStop', function(ressource)
|
|
if ressource ~= GetCurrentResourceName() then return end
|
|
SetNuiFocus(false, false)
|
|
end)
|