- Vorschau (3D-Ped/Kamera) in einen eigenen, verzoegerten Thread mit Laufnummer-Schutz ausgelagert. Damit laeuft die vernetzte Ped-Erzeugung nicht mehr zeitgleich mit dem Spieler-Teleport der Auswahl -> behebt "SendNetPacket called from multiple threads" (Crash beim Oeffnen). - Blackscreen nach dem Spawn behoben: der Fade wird nun allein vom Core gesteuert (Fade-out zu Beginn von spielerGeladen, Fade-in am Ende); SchliesseAuswahl blendet nicht mehr selbst -> kein Fade-Konflikt mehr. - Temporaere [AETHER-DEBUG]-Ausgaben wieder entfernt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
306 lines
12 KiB
Lua
306 lines
12 KiB
Lua
-- =====================================================================
|
|
-- Aether Core — Serverlogik
|
|
-- Verbindungsaufbau, Laden/Speichern der Spieler und Exports fuer Module.
|
|
-- =====================================================================
|
|
|
|
Aether = Aether or {}
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Hilfsfunktionen
|
|
-- ---------------------------------------------------------------------
|
|
|
|
--- Liest die Rockstar-Lizenz eines verbundenen Spielers aus
|
|
--- @param quelle number Server-ID
|
|
--- @return string|nil Lizenz (ohne Praefix) oder nil
|
|
local function HoleLizenz(quelle)
|
|
local lizenz = GetPlayerIdentifierByType(quelle, 'license')
|
|
if not lizenz then return nil end
|
|
return lizenz:gsub('license:', '')
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Verbindungsaufbau (Warteschlange / Bannpruefung)
|
|
-- ---------------------------------------------------------------------
|
|
AddEventHandler('playerConnecting', function(name, setKickReason, deferrals)
|
|
local quelle = source
|
|
deferrals.defer()
|
|
Wait(0)
|
|
|
|
deferrals.update('Aether — Verbindung wird geprueft ...')
|
|
|
|
local lizenz = HoleLizenz(quelle)
|
|
if not lizenz then
|
|
deferrals.done('Keine gueltige Rockstar-Lizenz gefunden. Bitte starte FiveM neu.')
|
|
return
|
|
end
|
|
|
|
-- Benutzer anlegen oder laden
|
|
local benutzer = Aether.DB.HoleOderErstelleBenutzer(lizenz, name)
|
|
if not benutzer then
|
|
deferrals.done('Datenbankfehler beim Laden deines Profils. Bitte melde dich beim Team.')
|
|
return
|
|
end
|
|
|
|
-- Bannpruefung
|
|
if benutzer.is_banned == 1 then
|
|
local text = benutzer.ban_reason or 'Kein Grund angegeben'
|
|
if benutzer.ban_until then
|
|
deferrals.done(('Du bist bis %s gesperrt.\nGrund: %s'):format(benutzer.ban_until, text))
|
|
else
|
|
deferrals.done(('Du bist dauerhaft gesperrt.\nGrund: %s'):format(text))
|
|
end
|
|
return
|
|
end
|
|
|
|
deferrals.update('Aether — Willkommen zurueck!')
|
|
Wait(200)
|
|
deferrals.done()
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Charakter in das Spiel laden
|
|
-- ---------------------------------------------------------------------
|
|
|
|
--- Erzeugt das Spielerobjekt aus einem Charakter-Datensatz und spawnt den Spieler
|
|
--- @param quelle number Server-ID
|
|
--- @param lizenz string Rockstar-Lizenz
|
|
--- @param charakter table Datensatz aus aether_characters
|
|
--- @return table Spielerobjekt
|
|
local function LadeUndSpawne(quelle, lizenz, charakter)
|
|
local spieler = Aether.ErzeugeSpieler(quelle, lizenz, charakter)
|
|
|
|
Aether.Info(('Spieler geladen: %s %s (ID %d, Charakter #%d)')
|
|
:format(spieler.vorname, spieler.nachname, quelle, spieler.charakterId))
|
|
|
|
-- Daten an den Client schicken, damit er spawnen kann
|
|
TriggerClientEvent('aether:client:spielerGeladen', quelle, spieler:HoleDaten())
|
|
|
|
-- Andere Ressourcen informieren (z. B. HUD, Inventar)
|
|
TriggerEvent('aether:server:spielerGeladen', quelle, spieler)
|
|
|
|
return spieler
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Spieler ist bereit (wird vom Client ausgeloest)
|
|
-- ---------------------------------------------------------------------
|
|
RegisterNetEvent('aether:server:spielerBereit', function()
|
|
local quelle = source
|
|
local lizenz = HoleLizenz(quelle)
|
|
if not lizenz then return end
|
|
|
|
-- Bereits geladen? Dann nichts tun (schuetzt vor doppeltem Laden)
|
|
if Aether.Spieler[quelle] then return end
|
|
|
|
-- Standardweg: `aether-character` zeigt die Charakterauswahl
|
|
if not Config.AutoCharakter then
|
|
TriggerEvent('aether:server:charakterAuswahlNoetig', quelle)
|
|
return
|
|
end
|
|
|
|
-- Notbetrieb ohne aether-character: Startcharakter anlegen und direkt laden
|
|
local charaktere = Aether.DB.HoleCharaktere(lizenz)
|
|
if #charaktere == 0 then
|
|
local spielerName = GetPlayerName(quelle)
|
|
local neueId = Aether.DB.ErstelleCharakter(lizenz, {
|
|
firstname = spielerName:sub(1, 32),
|
|
lastname = 'Neuling',
|
|
})
|
|
Aether.Info(('Startcharakter #%d fuer %s angelegt.'):format(neueId, spielerName))
|
|
charaktere = Aether.DB.HoleCharaktere(lizenz)
|
|
end
|
|
|
|
LadeUndSpawne(quelle, lizenz, charaktere[1])
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Position und Zustand vom Client uebernehmen
|
|
-- ---------------------------------------------------------------------
|
|
RegisterNetEvent('aether:server:aktualisierePosition', function(position)
|
|
local spieler = Aether.Spieler[source]
|
|
if not spieler or type(position) ~= 'table' then return end
|
|
spieler.position = position
|
|
end)
|
|
|
|
RegisterNetEvent('aether:server:aktualisiereZustand', function(zustand)
|
|
local spieler = Aether.Spieler[source]
|
|
if not spieler or type(zustand) ~= 'table' then return end
|
|
-- Nur bekannte Felder uebernehmen (Client darf nichts Beliebiges setzen)
|
|
if zustand.leben then spieler.leben = zustand.leben end
|
|
if zustand.hunger then spieler.hunger = Aether.Begrenze(zustand.hunger, 0, 100) end
|
|
if zustand.durst then spieler.durst = Aether.Begrenze(zustand.durst, 0, 100) end
|
|
if zustand.ausdauer then spieler.ausdauer = Aether.Begrenze(zustand.ausdauer, 0, 100) end
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Verbindungsabbruch: speichern und aufraeumen
|
|
-- ---------------------------------------------------------------------
|
|
AddEventHandler('playerDropped', function(grund)
|
|
local quelle = source
|
|
local spieler = Aether.Spieler[quelle]
|
|
if not spieler then return end
|
|
|
|
spieler:Speichere()
|
|
Aether.Info(('Spieler gespeichert und entfernt: %s %s (Grund: %s)')
|
|
:format(spieler.vorname, spieler.nachname, grund))
|
|
|
|
TriggerEvent('aether:server:spielerEntfernt', quelle, spieler)
|
|
Aether.Spieler[quelle] = nil
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Automatisches Speichern aller Spieler
|
|
-- ---------------------------------------------------------------------
|
|
CreateThread(function()
|
|
while true do
|
|
Wait(Config.SpeicherIntervall)
|
|
local anzahl = 0
|
|
for _, spieler in pairs(Aether.Spieler) do
|
|
spieler:Speichere()
|
|
anzahl = anzahl + 1
|
|
end
|
|
if anzahl > 0 then
|
|
Aether.Debug(('Automatisch gespeichert: %d Spieler.'):format(anzahl))
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Hunger und Durst langsam senken
|
|
-- ---------------------------------------------------------------------
|
|
CreateThread(function()
|
|
while true do
|
|
Wait(Config.Zustand.tickIntervall)
|
|
for _, spieler in pairs(Aether.Spieler) do
|
|
spieler:SetzeZustand('hunger', spieler.hunger - Config.Zustand.hungerProTick)
|
|
spieler:SetzeZustand('durst', spieler.durst - Config.Zustand.durstProTick)
|
|
|
|
-- Bei leerem Magen oder Durst Schaden zufuegen
|
|
if spieler.hunger <= Config.Zustand.schadenAbWert
|
|
or spieler.durst <= Config.Zustand.schadenAbWert then
|
|
TriggerClientEvent('aether:client:erleideSchaden', spieler.quelle,
|
|
Config.Zustand.schadenProTick)
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Beim Stoppen der Ressource alles sichern (z. B. bei /restart)
|
|
-- ---------------------------------------------------------------------
|
|
AddEventHandler('onResourceStop', function(ressource)
|
|
if ressource ~= GetCurrentResourceName() then return end
|
|
for _, spieler in pairs(Aether.Spieler) do
|
|
spieler:Speichere()
|
|
end
|
|
Aether.Info('Alle Spieler vor dem Stoppen gespeichert.')
|
|
end)
|
|
|
|
-- =====================================================================
|
|
-- EXPORTS — Schnittstelle fuer alle anderen Aether-Module
|
|
-- Beispiel: local spieler = exports['aether-core']:HoleSpieler(source)
|
|
-- =====================================================================
|
|
|
|
--- Liefert das Spielerobjekt zu einer Server-ID
|
|
exports('HoleSpieler', function(quelle)
|
|
return Aether.Spieler[quelle]
|
|
end)
|
|
|
|
--- Liefert alle aktiven Spielerobjekte
|
|
exports('HoleAlleSpieler', function()
|
|
return Aether.Spieler
|
|
end)
|
|
|
|
--- Laedt einen gewaehlten Charakter und spawnt den Spieler.
|
|
--- Wird von `aether-character` aufgerufen, nachdem der Spieler gewaehlt hat.
|
|
--- @param quelle number Server-ID
|
|
--- @param charakterId number Charakter-ID
|
|
--- @return boolean Erfolg
|
|
exports('LadeCharakter', function(quelle, charakterId)
|
|
local lizenz = HoleLizenz(quelle)
|
|
if not lizenz then return false end
|
|
|
|
-- Doppeltes Laden verhindern
|
|
if Aether.Spieler[quelle] then return false end
|
|
|
|
local charakter = Aether.DB.HoleCharakter(charakterId)
|
|
if not charakter then
|
|
Aether.Fehler(('Charakter #%s nicht gefunden.'):format(tostring(charakterId)))
|
|
return false
|
|
end
|
|
|
|
-- Sicherheitspruefung: Der Charakter muss dem Spieler gehoeren
|
|
if charakter.license ~= lizenz then
|
|
Aether.Fehler(('Spieler %d wollte fremden Charakter #%d laden.'):format(quelle, charakterId))
|
|
return false
|
|
end
|
|
|
|
LadeUndSpawne(quelle, lizenz, charakter)
|
|
return true
|
|
end)
|
|
|
|
--- Liefert das Spielerobjekt zu einer Charakter-ID
|
|
exports('HoleSpielerNachCharakter', function(charakterId)
|
|
for _, spieler in pairs(Aether.Spieler) do
|
|
if spieler.charakterId == charakterId then return spieler end
|
|
end
|
|
return nil
|
|
end)
|
|
|
|
--- Liefert die Server-ID zu einer Charakter-ID, falls der Spieler online ist.
|
|
--- Bewusst schlank: Exports serialisieren, ganze Spielerobjekte waeren teuer.
|
|
--- @param charakterId number Charakter-ID
|
|
--- @return number|nil Server-ID oder nil
|
|
exports('HoleQuelleNachCharakter', function(charakterId)
|
|
for quelle, spieler in pairs(Aether.Spieler) do
|
|
if spieler.charakterId == charakterId then return quelle end
|
|
end
|
|
return nil
|
|
end)
|
|
|
|
--- Liefert die Charakter-ID eines Spielers (oder nil)
|
|
--- @param quelle number Server-ID
|
|
--- @return number|nil
|
|
exports('HoleCharakterId', function(quelle)
|
|
local spieler = Aether.Spieler[quelle]
|
|
return spieler and spieler.charakterId or nil
|
|
end)
|
|
|
|
--- Liefert die Daten eines Spielers als einfache Tabelle.
|
|
--- Wichtig: Exports serialisieren — Methoden ueberleben die Grenze nicht,
|
|
--- daher hier bewusst nur reine Werte.
|
|
--- @param quelle number Server-ID
|
|
--- @return table|nil
|
|
exports('HoleSpielerInfo', function(quelle)
|
|
local spieler = Aether.Spieler[quelle]
|
|
if not spieler then return nil end
|
|
return spieler:HoleDaten()
|
|
end)
|
|
|
|
--- Aendert einen Zustandswert um einen Betrag (positiv oder negativ)
|
|
--- @param quelle number Server-ID
|
|
--- @param art string 'hunger' | 'durst' | 'ausdauer'
|
|
--- @param aenderung number Betrag
|
|
--- @return boolean Erfolg
|
|
exports('AendereZustand', function(quelle, art, aenderung)
|
|
local spieler = Aether.Spieler[quelle]
|
|
if not spieler then return false end
|
|
if art ~= 'hunger' and art ~= 'durst' and art ~= 'ausdauer' then return false end
|
|
spieler:SetzeZustand(art, (spieler[art] or 0) + aenderung)
|
|
return true
|
|
end)
|
|
|
|
--- Fuegt einem Spieler Geld hinzu
|
|
exports('FuegeGeldHinzu', function(quelle, art, betrag, grund)
|
|
local spieler = Aether.Spieler[quelle]
|
|
if not spieler then return false end
|
|
return spieler:FuegeGeldHinzu(art, betrag, grund)
|
|
end)
|
|
|
|
--- Zieht einem Spieler Geld ab (mit Deckungspruefung)
|
|
exports('EntferneGeld', function(quelle, art, betrag, grund)
|
|
local spieler = Aether.Spieler[quelle]
|
|
if not spieler then return false end
|
|
return spieler:EntferneGeld(art, betrag, grund)
|
|
end)
|