Charakterauswahl: Vorschau-Crash, Blackscreen und Debug beheben

- 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>
This commit is contained in:
Jerrit Fritzsche 2026-07-27 20:47:10 +02:00
parent cad096e7a6
commit 11cc043b6c
5 changed files with 56 additions and 39 deletions

View File

@ -18,8 +18,6 @@ local PARKPLATZ = { x = -1037.7, y = -2737.6, z = 20.2 }
--- @param charaktere table Liste der Charaktere --- @param charaktere table Liste der Charaktere
--- @param maxSlots number Maximale Anzahl Charaktere --- @param maxSlots number Maximale Anzahl Charaktere
local function OeffneAuswahl(charaktere, maxSlots) local function OeffneAuswahl(charaktere, maxSlots)
print(('[AETHER-DEBUG] client OeffneAuswahl: %d Charaktere')
:format(charaktere and #charaktere or -1))
auswahlOffen = true auswahlOffen = true
-- Eigenen Spieler unsichtbar parken — sichtbar ist nur der Vorschau-Ped -- Eigenen Spieler unsichtbar parken — sichtbar ist nur der Vorschau-Ped
@ -31,7 +29,6 @@ local function OeffneAuswahl(charaktere, maxSlots)
-- 3D-Vorschau mit dem Aussehen des ersten Charakters starten -- 3D-Vorschau mit dem Aussehen des ersten Charakters starten
local erstes = charaktere and charaktere[1] or nil local erstes = charaktere and charaktere[1] or nil
Vorschau.Starten(erstes and erstes.aussehen or nil, 'koerper') Vorschau.Starten(erstes and erstes.aussehen or nil, 'koerper')
print('[AETHER-DEBUG] client OeffneAuswahl: Vorschau gestartet, beende Ladebildschirm')
-- Ladebildschirm beenden, damit die Auswahl sichtbar wird -- Ladebildschirm beenden, damit die Auswahl sichtbar wird
ShutdownLoadingScreen() ShutdownLoadingScreen()
@ -53,10 +50,9 @@ local function SchliesseAuswahl()
SendNuiMessage(json.encode({ aktion = 'schliessen' })) SendNuiMessage(json.encode({ aktion = 'schliessen' }))
SetNuiFocus(false, false) SetNuiFocus(false, false)
DoScreenFadeOut(400)
Wait(400)
Vorschau.Beenden() Vorschau.Beenden()
-- Bewusst KEIN Fade hier: der Core blendet nach dem Spawn ein. Ein Fade
-- an dieser Stelle kollidiert mit dem Core-Fade -> Blackscreen.
end end
-- --------------------------------------------------------------------- -- ---------------------------------------------------------------------

View File

@ -22,6 +22,9 @@ local vorschauKamera= nil -- Handle der Kamera
local aktuellesModell = nil -- Aktuell geladenes Ped-Modell (fuer Geschlechtswechsel) local aktuellesModell = nil -- Aktuell geladenes Ped-Modell (fuer Geschlechtswechsel)
local aktuelleDrehung = 0.0 -- Zusaetzliche Drehung durch den Spieler local aktuelleDrehung = 0.0 -- Zusaetzliche Drehung durch den Spieler
local aktuellerModus = 'koerper' local aktuellerModus = 'koerper'
-- Laufnummer: entwertet noch laufende, verzoegerte Starts, wenn zwischendurch
-- beendet oder neu gestartet wird.
local aktuellerLauf = 0
-- --------------------------------------------------------------------- -- ---------------------------------------------------------------------
-- Interne Helfer -- Interne Helfer
@ -79,37 +82,53 @@ end
--- @param aussehen table|nil Startaussehen (bestimmt auch das Geschlecht) --- @param aussehen table|nil Startaussehen (bestimmt auch das Geschlecht)
--- @param modus string|nil Startansicht ('gesicht' | 'koerper') --- @param modus string|nil Startansicht ('gesicht' | 'koerper')
function Vorschau.Starten(aussehen, modus) function Vorschau.Starten(aussehen, modus)
-- Umgebung laden, damit der Ped nicht durch den Boden faellt aktuellerLauf = aktuellerLauf + 1
RequestCollisionAtCoord(VORSCHAU_POS.x, VORSCHAU_POS.y, VORSCHAU_POS.z) local lauf = aktuellerLauf
local modellName = AetherAussehen.HoleModell(aussehen and aussehen.geschlecht) -- WICHTIG: Ped und Kamera in einem EIGENEN Thread mit kurzer Verzoegerung
local modell = LadeModell(modellName) -- erzeugen. Sonst laeuft die (vernetzte) Ped-Erzeugung im selben Moment wie
if not modell then -- der Spieler-Ped-Teleport der Auswahl -> zwei Threads schreiben Netz-State
print('^1[Aether]^7 Vorschau-Modell konnte nicht geladen werden: ' .. modellName) -- ("SendNetPacket ... called from multiple threads") -> Client-Crash.
return CreateThread(function()
end Wait(250)
if lauf ~= aktuellerLauf then return end -- inzwischen beendet/neu gestartet
-- Ped nur lokal erzeugen (nicht im Netzwerk sichtbar) -- Umgebung laden, damit der Ped nicht durch den Boden faellt
vorschauPed = CreatePed(4, modell, VORSCHAU_POS.x, VORSCHAU_POS.y, VORSCHAU_POS.z, RequestCollisionAtCoord(VORSCHAU_POS.x, VORSCHAU_POS.y, VORSCHAU_POS.z)
0.0, false, false)
aktuellesModell = modellName
aktuelleDrehung = 0.0
SetEntityHeading(vorschauPed, 0.0) -- schaut Richtung Kamera local modellName = AetherAussehen.HoleModell(aussehen and aussehen.geschlecht)
FreezeEntityPosition(vorschauPed, true) local modell = LadeModell(modellName)
SetEntityInvincible(vorschauPed, true) if not modell then
SetBlockingOfNonTemporaryEvents(vorschauPed, true) print('^1[Aether]^7 Vorschau-Modell konnte nicht geladen werden: ' .. modellName)
SetModelAsNoLongerNeeded(modell) return
end
if lauf ~= aktuellerLauf then SetModelAsNoLongerNeeded(modell); return end
if aussehen then -- Ped nur lokal erzeugen (nicht im Netzwerk sichtbar)
AetherAussehen.Anwenden(vorschauPed, aussehen) vorschauPed = CreatePed(4, modell, VORSCHAU_POS.x, VORSCHAU_POS.y, VORSCHAU_POS.z,
end 0.0, false, false)
aktuellesModell = modellName
aktuelleDrehung = 0.0
-- Kamera erzeugen und aktivieren SetEntityHeading(vorschauPed, 0.0) -- schaut Richtung Kamera
vorschauKamera = CreateCam('DEFAULT_SCRIPTED_CAMERA', true) FreezeEntityPosition(vorschauPed, true)
SetzeKameraAuf(modus or 'koerper', false) SetEntityInvincible(vorschauPed, true)
SetCamActive(vorschauKamera, true) SetBlockingOfNonTemporaryEvents(vorschauPed, true)
RenderScriptCams(true, false, 0, true, true) SetModelAsNoLongerNeeded(modell)
if aussehen then
AetherAussehen.Anwenden(vorschauPed, aussehen)
end
-- Kamera erzeugen und aktivieren
vorschauKamera = CreateCam('DEFAULT_SCRIPTED_CAMERA', true)
SetzeKameraAuf(modus or 'koerper', false)
SetCamActive(vorschauKamera, true)
RenderScriptCams(true, false, 0, true, true)
-- Falls waehrenddessen beendet wurde: sofort wieder aufraeumen
if lauf ~= aktuellerLauf then Vorschau.Beenden() end
end)
end end
--- Uebertraegt ein Aussehen auf den Vorschau-Ped. --- Uebertraegt ein Aussehen auf den Vorschau-Ped.
@ -167,6 +186,9 @@ end
--- Beendet die Vorschau und raeumt Ped und Kamera auf --- Beendet die Vorschau und raeumt Ped und Kamera auf
function Vorschau.Beenden() function Vorschau.Beenden()
-- Noch laufende, verzoegerte Starts entwerten
aktuellerLauf = aktuellerLauf + 1
if vorschauKamera then if vorschauKamera then
RenderScriptCams(false, false, 0, true, true) RenderScriptCams(false, false, 0, true, true)
DestroyCam(vorschauKamera, false) DestroyCam(vorschauKamera, false)

View File

@ -111,13 +111,10 @@ end
-- Auswahl anzeigen (wird vom Core ausgeloest) -- Auswahl anzeigen (wird vom Core ausgeloest)
-- --------------------------------------------------------------------- -- ---------------------------------------------------------------------
AddEventHandler('aether:server:charakterAuswahlNoetig', function(quelle) AddEventHandler('aether:server:charakterAuswahlNoetig', function(quelle)
print(('[AETHER-DEBUG] character-server: charakterAuswahlNoetig empfangen fuer %s')
:format(tostring(quelle)))
local lizenz = HoleLizenz(quelle) local lizenz = HoleLizenz(quelle)
if not lizenz then print('[AETHER-DEBUG] character-server: keine Lizenz!'); return end if not lizenz then return end
TriggerClientEvent('aether:character:zeigeAuswahl', quelle, TriggerClientEvent('aether:character:zeigeAuswahl', quelle,
BaueListe(lizenz), Config.MaxCharaktere) BaueListe(lizenz), Config.MaxCharaktere)
print('[AETHER-DEBUG] character-server: zeigeAuswahl an Client gesendet')
end) end)
-- --------------------------------------------------------------------- -- ---------------------------------------------------------------------

View File

@ -52,6 +52,11 @@ end)
RegisterNetEvent('aether:client:spielerGeladen', function(daten) RegisterNetEvent('aether:client:spielerGeladen', function(daten)
Aether.SpielerDaten = daten Aether.SpielerDaten = daten
-- Waehrend des Spawns schwarz halten (versteckt Modellwechsel und den
-- Schnitt von der Auswahl-Kamera). Der Fade wird am Ende wieder geoeffnet
-- — der Core steuert den Fade allein, damit es keinen Konflikt gibt.
DoScreenFadeOut(0)
-- Passendes Standardmodell laden (bis der Charakter-Ersteller migriert ist) -- Passendes Standardmodell laden (bis der Charakter-Ersteller migriert ist)
local modellName = Config.StandardPed[daten.geschlecht] or Config.StandardPed.m local modellName = Config.StandardPed[daten.geschlecht] or Config.StandardPed.m
local modell = GetHashKey(modellName) local modell = GetHashKey(modellName)

View File

@ -87,8 +87,6 @@ end
RegisterNetEvent('aether:server:spielerBereit', function() RegisterNetEvent('aether:server:spielerBereit', function()
local quelle = source local quelle = source
local lizenz = HoleLizenz(quelle) local lizenz = HoleLizenz(quelle)
print(('[AETHER-DEBUG] spielerBereit: quelle=%s lizenz=%s AutoCharakter=%s')
:format(tostring(quelle), lizenz and 'ok' or 'nil', tostring(Config.AutoCharakter)))
if not lizenz then return end if not lizenz then return end
-- Bereits geladen? Dann nichts tun (schuetzt vor doppeltem Laden) -- Bereits geladen? Dann nichts tun (schuetzt vor doppeltem Laden)
@ -96,7 +94,6 @@ RegisterNetEvent('aether:server:spielerBereit', function()
-- Standardweg: `aether-character` zeigt die Charakterauswahl -- Standardweg: `aether-character` zeigt die Charakterauswahl
if not Config.AutoCharakter then if not Config.AutoCharakter then
print('[AETHER-DEBUG] spielerBereit: triggere charakterAuswahlNoetig')
TriggerEvent('aether:server:charakterAuswahlNoetig', quelle) TriggerEvent('aether:server:charakterAuswahlNoetig', quelle)
return return
end end