-- ===================================================================== -- Aether Garage — Serverlogik -- Verwaltet Fahrzeugbesitz, Ein- und Ausparken. Der Client darf ein -- Fahrzeug erst erzeugen, nachdem der Server es freigegeben hat. -- ===================================================================== local Core = exports['aether-core'] -- Fahrzeuge, die gerade ausgeparkt werden duerfen: [quelle] = kennzeichen local freigaben = {} -- Aktuell in der Welt stehende Fahrzeuge: [kennzeichen] = Entity-Handle -- Sie werden vom Server erzeugt und ueberleben dadurch auch, wenn kein -- Spieler in der Naehe ist. local aktiveFahrzeuge = {} -- --------------------------------------------------------------------- -- Fahrzeuge in der Welt erzeugen und entfernen -- --------------------------------------------------------------------- --- Erzeugt ein Fahrzeug serverseitig an einer Position --- @param datensatz table Zeile aus aether_vehicles --- @param x number --- @param y number --- @param z number --- @param heading number --- @return number|nil Entity-Handle local function ErzeugeFahrzeug(datensatz, x, y, z, heading) -- Bereits vorhandenes Fahrzeug mit diesem Kennzeichen zuerst entfernen local vorhandenes = aktiveFahrzeuge[datensatz.plate] if vorhandenes and DoesEntityExist(vorhandenes) then DeleteEntity(vorhandenes) end local fahrzeug = CreateVehicle(GetHashKey(datensatz.model), x, y, z, heading or 0.0, true, true) if not fahrzeug or fahrzeug == 0 then return nil end -- Warten, bis die Entitaet wirklich existiert local versuche = 0 while not DoesEntityExist(fahrzeug) and versuche < 50 do Wait(10) versuche = versuche + 1 end if not DoesEntityExist(fahrzeug) then return nil end local props = {} if datensatz.props then local erfolg, entschluesselt = pcall(json.decode, datensatz.props) if erfolg and type(entschluesselt) == 'table' then props = entschluesselt end end -- Alles, was nur clientseitig gesetzt werden kann, ueber den State Bag -- weitergeben. Der Client wendet es an, sobald das Fahrzeug bei ihm -- auftaucht (siehe client/main.lua). Entity(fahrzeug).state:set('aether', { kennzeichen = datensatz.plate, farbe = props.farbe, tank = datensatz.fuel, motor = datensatz.engine, karosserie = datensatz.body, }, true) aktiveFahrzeuge[datensatz.plate] = fahrzeug return fahrzeug end --- Entfernt ein Fahrzeug aus der Welt --- @param kennzeichen string Kennzeichen local function EntferneFahrzeug(kennzeichen) local fahrzeug = aktiveFahrzeuge[kennzeichen] if fahrzeug and DoesEntityExist(fahrzeug) then DeleteEntity(fahrzeug) end aktiveFahrzeuge[kennzeichen] = nil end -- --------------------------------------------------------------------- -- Hilfsfunktionen -- --------------------------------------------------------------------- --- Liefert die Charakter-ID eines Spielers local function CharakterVon(quelle) return Core:HoleCharakterId(quelle) end --- Prueft, ob ein Spieler Zugang zu einer Garage hat. --- Haus-Garagen werden spaeter ueber `aether-immobilien` geprueft. --- @param quelle number Server-ID --- @param garageId string Kennung der Garage --- @return boolean local function HatZugang(quelle, garageId) local garage = Garage.Garagen[garageId] if not garage then return false end if garage.typ == 'oeffentlich' then return true end -- Optionale Abfrage beim Immobilien-Modul local erfolg, ergebnis = pcall(function() return exports['aether-immobilien']:HatGaragenZugang(quelle, garageId) end) if erfolg and type(ergebnis) == 'boolean' then return ergebnis end -- Modul noch nicht vorhanden: Haus-Garagen bleiben verschlossen return false end --- Laedt alle Fahrzeuge eines Charakters --- @param charakterId number Charakter-ID --- @return table Liste von Fahrzeugen local function HoleFahrzeuge(charakterId) local zeilen = MySQL.query.await([[ SELECT plate, model, props, garage, state, fuel, engine, body, mileage FROM aether_vehicles WHERE owner = ? ]], { charakterId }) or {} local liste = {} for _, zeile in ipairs(zeilen) do local props = {} if zeile.props then local erfolg, entschluesselt = pcall(json.decode, zeile.props) if erfolg and type(entschluesselt) == 'table' then props = entschluesselt end end liste[#liste + 1] = { kennzeichen = zeile.plate, modell = zeile.model, name = props.name or zeile.model, klasse = props.klasse or 'Fahrzeug', typ = props.typ or 'sedan', farbe = props.farbe or '#20232a', plateStyle = props.plateStyle or 'sa', garage = zeile.garage, status = zeile.state, -- in | out | impound tank = math.floor(zeile.fuel or 0), motor = math.floor((zeile.engine or 1000) / 10), karosserie = math.floor((zeile.body or 1000) / 10), km = zeile.mileage or 0, } end return liste end --- Baut den Zustand fuer die Oberflaeche --- @param quelle number Server-ID --- @param standort string Garage, an der der Spieler steht --- @return table|nil local function BaueZustand(quelle, standort) local charakterId = CharakterVon(quelle) if not charakterId then return nil end -- Nur zugaengliche Garagen anzeigen local garagen = {} for garageId, garage in pairs(Garage.Garagen) do if HatZugang(quelle, garageId) then garagen[#garagen + 1] = { id = garageId, name = garage.name, typ = garage.typ, } end end return { garagen = garagen, fahrzeuge = HoleFahrzeuge(charakterId), standort = standort, gebuehr = Garage.AUSLOESE_GEBUEHR, } end --- Schickt den Zustand an einen Spieler local function SendeZustand(quelle, standort, hinweis) local zustand = BaueZustand(quelle, standort) if not zustand then return end zustand.hinweis = hinweis TriggerClientEvent('aether:garage:zustand', quelle, zustand) end --- Lehnt eine Aktion ab local function Ablehnen(quelle, grund) TriggerClientEvent('aether:garage:fehler', quelle, grund) end --- Liest ein Fahrzeug und prueft den Besitz --- @param charakterId number Charakter-ID --- @param kennzeichen string Kennzeichen --- @return table|nil local function HoleEigenes(charakterId, kennzeichen) local treffer = MySQL.query.await( 'SELECT * FROM aether_vehicles WHERE plate = ? AND owner = ?', { kennzeichen, charakterId }) return treffer and treffer[1] or nil end -- --------------------------------------------------------------------- -- Ausgeparkte Fahrzeuge beim Serverstart wiederherstellen -- --------------------------------------------------------------------- CreateThread(function() Wait(2000) -- warten, bis Datenbank und Spielwelt bereit sind local zeilen = MySQL.query.await([[ SELECT * FROM aether_vehicles WHERE state = 'out' AND pos_x IS NOT NULL ]]) or {} local wiederhergestellt = 0 for _, zeile in ipairs(zeilen) do local entitaet = ErzeugeFahrzeug(zeile, zeile.pos_x, zeile.pos_y, zeile.pos_z, zeile.heading) if entitaet then wiederhergestellt = wiederhergestellt + 1 else -- Konnte es nicht erzeugt werden (z. B. Modell entfernt), -- wandert es zurueck in seine Garage statt verloren zu gehen. MySQL.update("UPDATE aether_vehicles SET state = 'in' WHERE plate = ?", { zeile.plate }) Aether.Fehler(('Fahrzeug %s konnte nicht gesetzt werden — zurueck in die Garage.') :format(zeile.plate)) end Wait(50) -- Erzeugung entzerren end if wiederhergestellt > 0 then Aether.Info(('%d ausgeparkte Fahrzeuge wiederhergestellt.'):format(wiederhergestellt)) end end) -- --------------------------------------------------------------------- -- Position der ausgeparkten Fahrzeuge regelmaessig sichern -- --------------------------------------------------------------------- CreateThread(function() while true do Wait(30000) -- alle 30 Sekunden for kennzeichen, fahrzeug in pairs(aktiveFahrzeuge) do if DoesEntityExist(fahrzeug) then local position = GetEntityCoords(fahrzeug) MySQL.update([[ UPDATE aether_vehicles SET pos_x = ?, pos_y = ?, pos_z = ?, heading = ? WHERE plate = ? ]], { position.x, position.y, position.z, GetEntityHeading(fahrzeug), kennzeichen }) else -- Entitaet ist verschwunden (z. B. zerstoert): Eintrag aufraeumen aktiveFahrzeuge[kennzeichen] = nil end end end end) --- Beim Stoppen der Ressource die letzten Positionen sichern AddEventHandler('onResourceStop', function(ressource) if ressource ~= GetCurrentResourceName() then return end for kennzeichen, fahrzeug in pairs(aktiveFahrzeuge) do if DoesEntityExist(fahrzeug) then local position = GetEntityCoords(fahrzeug) MySQL.update.await([[ UPDATE aether_vehicles SET pos_x = ?, pos_y = ?, pos_z = ?, heading = ? WHERE plate = ? ]], { position.x, position.y, position.z, GetEntityHeading(fahrzeug), kennzeichen }) end end Aether.Info('Fahrzeugpositionen gesichert.') end) -- --------------------------------------------------------------------- -- Zustand eines fahrenden Fahrzeugs uebernehmen (vom Client gemeldet) -- --------------------------------------------------------------------- RegisterNetEvent('aether:garage:zustandMelden', function(kennzeichen, zustand) if type(kennzeichen) ~= 'string' or type(zustand) ~= 'table' then return end if not aktiveFahrzeuge[kennzeichen] then return end -- nur bekannte Fahrzeuge local tank = math.max(0, math.min(100, tonumber(zustand.tank) or 100)) local motor = math.max(0, math.min(1000, tonumber(zustand.motor) or 1000)) local body = math.max(0, math.min(1000, tonumber(zustand.karosserie) or 1000)) MySQL.update('UPDATE aether_vehicles SET fuel = ?, engine = ?, body = ? WHERE plate = ?', { tank, motor, body, kennzeichen }) end) -- --------------------------------------------------------------------- -- Startfahrzeug fuer neue Charaktere -- --------------------------------------------------------------------- AddEventHandler('aether:server:spielerGeladen', function(quelle, spieler) if not Garage.STARTFAHRZEUG then return end local vorhanden = MySQL.scalar.await( 'SELECT COUNT(*) FROM aether_vehicles WHERE owner = ?', { spieler.charakterId }) if (vorhanden or 0) > 0 then return end local kennzeichen = Garage.ErzeugeKennzeichen() MySQL.insert.await([[ INSERT INTO aether_vehicles (plate, owner, model, props, garage, state) VALUES (?, ?, ?, ?, ?, 'in') ]], { kennzeichen, spieler.charakterId, Garage.STARTFAHRZEUG.modell, json.encode({ name = 'Blista', klasse = 'Kleinwagen', typ = 'compact', farbe = '#3563a8', plateStyle = 'sa' }), Garage.STARTFAHRZEUG.garage, }) Aether.Info(('Startfahrzeug %s fuer Charakter #%d angelegt.') :format(kennzeichen, spieler.charakterId)) end) -- --------------------------------------------------------------------- -- Garage oeffnen -- --------------------------------------------------------------------- RegisterNetEvent('aether:garage:oeffnen', function(garageId) local quelle = source if not CharakterVon(quelle) then return end if type(garageId) ~= 'string' or not Garage.Garagen[garageId] then return end if not HatZugang(quelle, garageId) then Ablehnen(quelle, 'Zu dieser Garage hast du keinen Zugang.') return end -- Entfernungspruefung local position = GetEntityCoords(GetPlayerPed(quelle)) if #(position - Garage.Garagen[garageId].position) > (Garage.INTERAKTION_WEITE + 3.0) then Ablehnen(quelle, 'Du bist nicht an dieser Garage.') return end local zustand = BaueZustand(quelle, garageId) if zustand then TriggerClientEvent('aether:garage:zeige', quelle, zustand) end end) -- --------------------------------------------------------------------- -- Ausparken -- --------------------------------------------------------------------- RegisterNetEvent('aether:garage:ausparken', function(kennzeichen, standort) local quelle = source local charakterId = CharakterVon(quelle) if not charakterId then return end if type(kennzeichen) ~= 'string' or type(standort) ~= 'string' then return end local garage = Garage.Garagen[standort] if not garage or not HatZugang(quelle, standort) then return end local fahrzeug = HoleEigenes(charakterId, kennzeichen) if not fahrzeug then Ablehnen(quelle, 'Das ist nicht dein Fahrzeug.') return end if fahrzeug.state == 'impound' then Ablehnen(quelle, 'Das Fahrzeug ist beschlagnahmt.') return end if fahrzeug.state == 'out' then Ablehnen(quelle, 'Das Fahrzeug steht bereits draußen.') return end -- Standort-Regel: nur an der Garage, wo das Fahrzeug wirklich steht if fahrzeug.garage ~= standort then local wo = Garage.Garagen[fahrzeug.garage] Ablehnen(quelle, ('Das Fahrzeug steht in %s.'):format(wo and wo.name or fahrzeug.garage)) return end -- Fahrzeug serverseitig erzeugen, damit es unabhaengig vom Spieler -- (und ueber Neustarts hinweg) in der Welt bleibt local spawn = garage.spawn local entitaet = ErzeugeFahrzeug(fahrzeug, spawn.x, spawn.y, spawn.z, spawn.w) if not entitaet then Ablehnen(quelle, 'Fahrzeug konnte nicht erzeugt werden.') return end MySQL.update.await([[ UPDATE aether_vehicles SET state = 'out', pos_x = ?, pos_y = ?, pos_z = ?, heading = ? WHERE plate = ? ]], { spawn.x, spawn.y, spawn.z, spawn.w, kennzeichen }) freigaben[quelle] = kennzeichen -- Dem Client sagen, in welches Fahrzeug er einsteigen soll TriggerClientEvent('aether:garage:einsteigen', quelle, NetworkGetNetworkIdFromEntity(entitaet), kennzeichen) end) -- --------------------------------------------------------------------- -- Einparken -- --------------------------------------------------------------------- RegisterNetEvent('aether:garage:einparken', function(kennzeichen, standort, zustand) local quelle = source local charakterId = CharakterVon(quelle) if not charakterId then return end if type(kennzeichen) ~= 'string' or type(standort) ~= 'string' then return end local garage = Garage.Garagen[standort] if not garage or not HatZugang(quelle, standort) then return end local fahrzeug = HoleEigenes(charakterId, kennzeichen) if not fahrzeug then Ablehnen(quelle, 'Das ist nicht dein Fahrzeug.') return end -- Entfernung des Spielers zur Garage pruefen local position = GetEntityCoords(GetPlayerPed(quelle)) if #(position - garage.position) > Garage.EINPARK_WEITE then Ablehnen(quelle, 'Du bist zu weit von der Garage entfernt.') return end -- Zustandswerte vom Client uebernehmen, aber begrenzen zustand = type(zustand) == 'table' and zustand or {} local tank = math.max(0, math.min(100, tonumber(zustand.tank) or fahrzeug.fuel or 100)) local motor = math.max(0, math.min(1000, tonumber(zustand.motor) or fahrzeug.engine or 1000)) local body = math.max(0, math.min(1000, tonumber(zustand.karosserie) or fahrzeug.body or 1000)) -- Kilometerstand: gefahrene Meter aufaddieren (nie verringern). -- Nach oben begrenzt, damit manipulierte Werte nichts kaputtmachen. local meter = math.max(0, math.min(1000000, math.floor(tonumber(zustand.meter) or 0))) local km = (fahrzeug.mileage or 0) + math.floor(meter / 1000) MySQL.update.await([[ UPDATE aether_vehicles SET state = 'in', garage = ?, fuel = ?, engine = ?, body = ?, mileage = ?, pos_x = NULL, pos_y = NULL, pos_z = NULL, heading = NULL WHERE plate = ? ]], { standort, tank, motor, body, km, kennzeichen }) -- Fahrzeug aus der Welt nehmen (serverseitig, damit es bei allen verschwindet) EntferneFahrzeug(kennzeichen) freigaben[quelle] = nil SendeZustand(quelle, standort, ('Fahrzeug in %s eingelagert.'):format(garage.name)) end) -- --------------------------------------------------------------------- -- Beschlagnahmtes Fahrzeug ausloesen -- --------------------------------------------------------------------- RegisterNetEvent('aether:garage:ausloesen', function(kennzeichen, standort) local quelle = source local charakterId = CharakterVon(quelle) if not charakterId then return end if type(kennzeichen) ~= 'string' or type(standort) ~= 'string' then return end if not Garage.Garagen[standort] or not HatZugang(quelle, standort) then return end local fahrzeug = HoleEigenes(charakterId, kennzeichen) if not fahrzeug or fahrzeug.state ~= 'impound' then return end if not Core:EntferneGeld(quelle, 'bargeld', Garage.AUSLOESE_GEBUEHR, 'Fahrzeug ausgelöst') then Ablehnen(quelle, ('Du brauchst %d € in bar.'):format(Garage.AUSLOESE_GEBUEHR)) return end MySQL.update.await( 'UPDATE aether_vehicles SET state = ?, garage = ? WHERE plate = ?', { 'in', standort, kennzeichen }) SendeZustand(quelle, standort, ('Fahrzeug für %d € ausgelöst.'):format(Garage.AUSLOESE_GEBUEHR)) end) -- --------------------------------------------------------------------- -- Aufraeumen -- --------------------------------------------------------------------- AddEventHandler('playerDropped', function() freigaben[source] = nil end) -- ===================================================================== -- EXPORTS -- ===================================================================== --- Gibt einem Charakter ein Fahrzeug --- @param charakterId number Charakter-ID --- @param modell string Spawnname des Modells --- @param garageId string Garage, in der es steht --- @param props table|nil Anzeigedaten (Name, Klasse, Farbe ...) --- @return string|nil Kennzeichen exports('GebeFahrzeug', function(charakterId, modell, garageId, props) if type(charakterId) ~= 'number' or type(modell) ~= 'string' then return nil end local kennzeichen = Garage.ErzeugeKennzeichen() MySQL.insert.await([[ INSERT INTO aether_vehicles (plate, owner, model, props, garage, state) VALUES (?, ?, ?, ?, ?, 'in') ]], { kennzeichen, charakterId, modell, json.encode(props or { name = modell }), garageId or 'legion', }) return kennzeichen end) --- Beschlagnahmt ein Fahrzeug (fuer das Polizei-CAD) --- @param kennzeichen string Kennzeichen --- @return boolean Erfolg exports('Beschlagnahmen', function(kennzeichen) if type(kennzeichen) ~= 'string' then return false end MySQL.update.await( "UPDATE aether_vehicles SET state = 'impound' WHERE plate = ?", { kennzeichen }) return true end) --- Sucht den Besitzer eines Kennzeichens (fuer die Kennzeichenabfrage im CAD) --- @param kennzeichen string Kennzeichen --- @return table|nil { charakterId, vorname, nachname, modell } exports('HoleHalter', function(kennzeichen) local treffer = MySQL.query.await([[ SELECT v.owner, v.model, c.firstname, c.lastname FROM aether_vehicles v JOIN aether_characters c ON c.id = v.owner WHERE v.plate = ? ]], { kennzeichen }) if not treffer or not treffer[1] then return nil end return { charakterId = treffer[1].owner, vorname = treffer[1].firstname, nachname = treffer[1].lastname, modell = treffer[1].model, } end)