Der Core schaltete Auto-Spawn ab, erzwang aber keinen kontrollierten Spawn und fasste den Ped sofort an. Dadurch lief die unkontrollierte Spawn-Erzeugung des spawnmanagers parallel zur Ped-Manipulation von Core/Character -> zwei Threads schrieben gleichzeitig Netz-State: "SendNetPacket called from multiple threads" -> Client-Crash beim Charaktermenue (schwarzer Bildschirm, dann Absturz). Fix: Auto-Spawn aus, EINMAL kontrolliert via spawnPlayer spawnen, auf den Callback warten und den Ped erst danach unsichtbar/geparkt setzen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
196 lines
7.3 KiB
Lua
196 lines
7.3 KiB
Lua
-- =====================================================================
|
|
-- Aether Core — Clientlogik
|
|
-- Meldet sich beim Server an, spawnt den Charakter und haelt die Daten aktuell.
|
|
-- =====================================================================
|
|
|
|
Aether = Aether or {}
|
|
|
|
-- Daten des eigenen Charakters (vom Server befuellt)
|
|
Aether.SpielerDaten = nil
|
|
-- Ist der Spieler fertig geladen und im Spiel?
|
|
Aether.IstGeladen = false
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Startvorgang
|
|
-- ---------------------------------------------------------------------
|
|
CreateThread(function()
|
|
-- Warten, bis die Spielwelt bereit ist
|
|
while not NetworkIsSessionStarted() do
|
|
Wait(100)
|
|
end
|
|
|
|
-- Bildschirm sofort schwarz halten, bevor irgendetwas spawnt
|
|
DoScreenFadeOut(0)
|
|
|
|
-- WICHTIG: den Spawn deterministisch selbst steuern.
|
|
-- Ohne einen kontrollierten Spawn liefert der spawnmanager den Ped
|
|
-- unkontrolliert (Auto-Spawn) — dann fasst der Core/Character-Flow den
|
|
-- Ped an, WÄHREND die Netzwerk-Erzeugung noch läuft. Zwei Threads
|
|
-- schreiben dann gleichzeitig Netz-State:
|
|
-- "SendNetPacket ... called from multiple threads" -> Client-Crash.
|
|
-- Lösung: Auto-Spawn AUS, EINMAL kontrolliert spawnen, und den Ped erst
|
|
-- NACH dem Spawn-Callback anfassen.
|
|
local spawnFertig = false
|
|
local hatSpawnmanager = pcall(function()
|
|
exports.spawnmanager:setAutoSpawn(false)
|
|
exports.spawnmanager:spawnPlayer({
|
|
x = Config.SpawnPunkt.x,
|
|
y = Config.SpawnPunkt.y,
|
|
z = Config.SpawnPunkt.z,
|
|
heading = Config.SpawnPunkt.heading or 0.0,
|
|
model = 'mp_m_freemode_01',
|
|
skipFade = true, -- wir steuern den Fade selbst
|
|
}, function()
|
|
spawnFertig = true
|
|
end)
|
|
end)
|
|
|
|
if hatSpawnmanager then
|
|
-- Auf den Spawn-Callback warten (mit Sicherheitslimit ~5 s)
|
|
local versuche = 0
|
|
while not spawnFertig and versuche < 100 do
|
|
Wait(50)
|
|
versuche = versuche + 1
|
|
end
|
|
else
|
|
Aether.Debug('spawnmanager nicht verfuegbar — Core spawnt eigenstaendig.')
|
|
end
|
|
|
|
-- Jetzt existiert ein gültiger, gestreamter Ped. Erst JETZT anfassen:
|
|
-- unsichtbar und geparkt halten, bis der Charakter gewählt ist.
|
|
local ped = PlayerPedId()
|
|
SetEntityVisible(ped, false, false)
|
|
FreezeEntityPosition(ped, true)
|
|
|
|
Aether.Debug('Client bereit — fordere Spielerdaten an.')
|
|
TriggerServerEvent('aether:server:spielerBereit')
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Spieler wurde geladen: Ped setzen und spawnen
|
|
-- ---------------------------------------------------------------------
|
|
RegisterNetEvent('aether:client:spielerGeladen', function(daten)
|
|
Aether.SpielerDaten = daten
|
|
|
|
-- Passendes Standardmodell laden (bis der Charakter-Ersteller migriert ist)
|
|
local modellName = Config.StandardPed[daten.geschlecht] or Config.StandardPed.m
|
|
local modell = GetHashKey(modellName)
|
|
|
|
RequestModel(modell)
|
|
local versuche = 0
|
|
while not HasModelLoaded(modell) and versuche < 100 do
|
|
Wait(50)
|
|
versuche = versuche + 1
|
|
end
|
|
|
|
if HasModelLoaded(modell) then
|
|
SetPlayerModel(PlayerId(), modell)
|
|
SetModelAsNoLongerNeeded(modell)
|
|
else
|
|
Aether.Fehler('Ped-Modell konnte nicht geladen werden: ' .. modellName)
|
|
end
|
|
|
|
-- Spieler an die gespeicherte Position setzen
|
|
local ped = PlayerPedId()
|
|
local pos = daten.position or Config.SpawnPunkt
|
|
SetEntityCoordsNoOffset(ped, pos.x + 0.0, pos.y + 0.0, pos.z + 0.0, false, false, false)
|
|
SetEntityHeading(ped, pos.heading or 0.0)
|
|
|
|
-- Zustand wiederherstellen
|
|
SetEntityHealth(ped, daten.leben or 200)
|
|
FreezeEntityPosition(ped, false)
|
|
SetEntityVisible(ped, true, false)
|
|
|
|
-- Warten, bis die Umgebung geladen ist, dann Bild freigeben
|
|
local wartezeit = 0
|
|
while not HasCollisionLoadedAroundEntity(ped) and wartezeit < 100 do
|
|
Wait(50)
|
|
wartezeit = wartezeit + 1
|
|
end
|
|
|
|
ShutdownLoadingScreen()
|
|
ShutdownLoadingScreenNui()
|
|
DoScreenFadeIn(1000)
|
|
|
|
Aether.IstGeladen = true
|
|
Aether.Info(('Willkommen, %s %s!'):format(daten.vorname, daten.nachname))
|
|
|
|
-- Andere Ressourcen (HUD, Handy, Inventar) informieren.
|
|
-- WICHTIG: anderer Name als das Netz-Event, sonst ruft sich dieser
|
|
-- Handler endlos selbst auf.
|
|
TriggerEvent('aether:client:bereit', daten)
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Aktualisierungen vom Server
|
|
-- ---------------------------------------------------------------------
|
|
RegisterNetEvent('aether:client:geldAktualisiert', function(geld)
|
|
if not Aether.SpielerDaten then return end
|
|
Aether.SpielerDaten.bargeld = geld.bargeld
|
|
Aether.SpielerDaten.konto = geld.konto
|
|
Aether.SpielerDaten.schwarzgeld = geld.schwarzgeld
|
|
-- Weiterreichen ans HUD
|
|
TriggerEvent('aether:client:geldGeaendert', geld)
|
|
end)
|
|
|
|
RegisterNetEvent('aether:client:zustandAktualisiert', function(zustand)
|
|
if not Aether.SpielerDaten then return end
|
|
Aether.SpielerDaten.hunger = zustand.hunger
|
|
Aether.SpielerDaten.durst = zustand.durst
|
|
Aether.SpielerDaten.ausdauer = zustand.ausdauer
|
|
-- Weiterreichen ans HUD
|
|
TriggerEvent('aether:client:zustandGeaendert', zustand)
|
|
end)
|
|
|
|
--- Schaden durch Hunger/Durst zufuegen
|
|
RegisterNetEvent('aether:client:erleideSchaden', function(menge)
|
|
local ped = PlayerPedId()
|
|
local leben = GetEntityHealth(ped)
|
|
SetEntityHealth(ped, math.max(0, leben - (menge or 5)))
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Position regelmaessig an den Server melden (fuer das Speichern)
|
|
-- ---------------------------------------------------------------------
|
|
CreateThread(function()
|
|
while true do
|
|
Wait(10000) -- alle 10 Sekunden
|
|
if Aether.IstGeladen then
|
|
local ped = PlayerPedId()
|
|
local koordinaten = GetEntityCoords(ped)
|
|
TriggerServerEvent('aether:server:aktualisierePosition', {
|
|
x = Aether.Runde(koordinaten.x, 2),
|
|
y = Aether.Runde(koordinaten.y, 2),
|
|
z = Aether.Runde(koordinaten.z, 2),
|
|
heading = Aether.Runde(GetEntityHeading(ped), 2),
|
|
})
|
|
-- Lebenswert mitschicken, damit er beim Speichern stimmt
|
|
TriggerServerEvent('aether:server:aktualisiereZustand', {
|
|
leben = GetEntityHealth(ped),
|
|
})
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Kein Charakter vorhanden (spaeter uebernimmt das aether-character)
|
|
-- ---------------------------------------------------------------------
|
|
RegisterNetEvent('aether:client:keinCharakter', function()
|
|
Aether.Fehler('Kein Charakter vorhanden — Charaktererstellung ist noch nicht migriert.')
|
|
end)
|
|
|
|
-- =====================================================================
|
|
-- EXPORTS fuer andere Module
|
|
-- Beispiel: local daten = exports['aether-core']:HoleSpielerDaten()
|
|
-- =====================================================================
|
|
|
|
--- Liefert die Daten des eigenen Charakters
|
|
exports('HoleSpielerDaten', function()
|
|
return Aether.SpielerDaten
|
|
end)
|
|
|
|
--- Ist der Spieler fertig geladen?
|
|
exports('IstGeladen', function()
|
|
return Aether.IstGeladen
|
|
end)
|