Files
d4rk_divegear/client/main.lua
T
D4rkst3randClaude Opus 5.5 d4870444df fix: nach dem Patronentausch immer neu einlesen
Gleiche Groesse gegen gleiche Groesse galt als dieselbe Patrone - die Konsole
zeigte weiter die alte Restluft (Betreiber, 29.09.2026).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 00:28:05 +02:00

722 lines
24 KiB
Lua

local config = require 'config.client'
local shared = require 'config.shared'
--- Tauchgeraet bereit: Flasche mit Patrone UND Maske angezogen (28.09.2026 -
--- vorher hingen hier zwei Props, mask und tank).
local currentGear = {
enabled = false
}
--- Laufnummer der Faeden (Verbrauch, HUD, Waechter). Wird beim Tauschen einer
--- Patrone sofort neu angelegt - ohne Nummer liefen die alten Faeden nach
--- ihrem Wait einfach weiter, und es gaebe sie doppelt.
local gearLauf = 0
--- Lampe an? Der eigentliche Lichtkegel wird in client/light.lua gezeichnet,
--- gesteuert über den replizierten Statebag.
local lampOn = false
--- Aktuell getragene Flasche. { slot, name, label, oxygen, capacity }
--- oxygen sind Restsekunden, kein Prozentwert.
local activeTank = nil
--- Bereits ausgelöste Warnschwellen des laufenden Tauchgangs.
local warned = {}
--- Geglättete Tauchtiefe in Metern. Wird vom HUD-Thread gepflegt und sowohl für
--- die Anzeige als auch für den tiefenabhängigen Verbrauch benutzt.
local currentDepth = 0.0
--- Grenzen für die Farbstufen im HUD, aus config.warnAtSeconds abgeleitet.
local warnThreshold, dangerThreshold = 0, 0
for i = 1, #config.warnAtSeconds do
local threshold = config.warnAtSeconds[i]
if threshold > warnThreshold then warnThreshold = threshold end
if dangerThreshold == 0 or threshold < dangerThreshold then dangerThreshold = threshold end
end
--- @param seconds number
--- @return string mm:ss
local function formatTime(seconds)
seconds = math.max(0, math.floor(seconds))
return ('%02d:%02d'):format(seconds // 60, seconds % 60)
end
--- Aktuelle Tauchtiefe in Metern. 0 wenn kein Wasser über dem Ped ist.
local function getDepth()
local coords = GetEntityCoords(cache.ped)
local hasWater, waterZ = GetWaterHeight(coords.x, coords.y, coords.z)
if not hasWater then return 0.0 end
return math.max(0.0, waterZ - coords.z)
end
--- Aktuell aktive Tiefenrausch-Stufe (Index in config.narcosis.stages), 0 = keine.
local narcosisStage = 0
local narcosisWarned = false
local narcosisDamageBuffer = 0.0
local engineDepthWarned = false
--- Wie weit die aktuelle Tiefe unter der Grenze der getragenen Flasche liegt.
--- @return number Meter unterhalb der Grenze, negativ solange alles gut ist
local function getDepthOverLimit()
if not activeTank or not activeTank.maxDepth then return -math.huge end
return currentDepth - activeTank.maxDepth
end
--- Setzt Bildschirmeffekt und Kamerawackeln auf die angegebene Stufe.
--- @param stage number 0 = alles aus
local function applyNarcosisStage(stage)
if stage == narcosisStage then return end
narcosisStage = stage
ClearTimecycleModifier()
StopGameplayCamShaking(true)
local data = stage > 0 and config.narcosis.stages[stage]
if not data then return end
if data.timecycle then
SetTimecycleModifier(data.timecycle)
SetTimecycleModifierStrength(data.strength or 1.0)
end
if data.shake then
ShakeGameplayCam(data.shake, data.shakeIntensity or 0.5)
end
end
--- Verbrauchsfaktor. Tiefer tauchen kostet mehr Luft.
--- Nutzt die geglättete Tiefe aus dem HUD-Thread, damit Wellengang den Verbrauch
--- nicht im Sekundentakt springen lässt.
local function getDecayFactor()
local factor = 1.0
if config.depthDecayEnabled then
factor = math.min(config.maxDecayFactor, 1.0 + currentDepth / config.depthDecayReference)
end
-- Tiefenrausch: unter Stress atmet man mehr.
local stage = narcosisStage > 0 and config.narcosis.stages[narcosisStage]
if stage then
factor = factor * (stage.drainFactor or 1.0)
end
return factor
end
--- @param ped number? default cache.ped - beim Model-Wechsel muss der neue Ped explizit rein
local function enableScuba(ped)
ped = ped or cache.ped
SetEnableScuba(ped, true)
SetPedMaxTimeUnderwater(ped, 2000.00)
end
--- @param outOfAir boolean? true wenn die Flasche leer ist statt die Ausrüstung abgelegt wurde
--- @param ped number? default cache.ped
local function disableScuba(outOfAir, ped)
ped = ped or cache.ped
SetEnableScuba(ped, false)
SetPedMaxTimeUnderwater(ped, outOfAir and config.maxTimeUnderwaterOutOfAir or config.maxTimeUnderwater)
end
--- @param state boolean
--- @param notifyIfUnavailable boolean? Rückmeldung, wenn das Outfit keine Lampe hat
local function setLamp(state, notifyIfUnavailable)
if not currentGear.enabled then state = false end
lampOn = state
-- Repliziert: client/light.lua setzt daraus den Lampen-Flag bei allen Tauchern.
LocalPlayer.state:set('divelight', state, true)
-- Lokal sofort setzen statt auf den Abgleich-Thread zu warten.
SetEnableScubaGearLight(cache.ped, state)
-- Sicherheitsnetz: die Native greift normalerweise auch ohne Scuba-Kleidung,
-- aber falls sie bei einem Ped-Model doch nicht anschlägt, ist eine Meldung
-- besser als ein scheinbar kaputter Keybind.
if state and notifyIfUnavailable and not IsScubaGearLightEnabled(cache.ped) then
lib.notify({ description = locale('error.no_lamp_on_outfit'), type = 'error' })
end
end
--- Schreibt die Restluft in die Metadata der Flasche. Der Server clampt den Wert
--- und gibt den akzeptierten Stand zurück, damit Client und Inventar nicht auseinanderlaufen.
--- @param immediate boolean? true = Fire-and-Forget (Tod, Resource-Stop)
local function syncTank(immediate)
if not activeTank then return end
if immediate then
TriggerServerEvent('d4rk_divegear:server:syncTankNow', activeTank.slot, activeTank.oxygen)
return
end
local accepted = lib.callback.await('d4rk_divegear:server:syncTank', false, activeTank.slot, activeTank.oxygen)
if accepted and activeTank then
activeTank.oxygen = accepted
end
end
--- Gemeinsamer Teardown für Ablegen, leere Flasche und Tod.
--- @param outOfAir boolean?
--- @param immediateSync boolean?
--- @param ohneSync boolean? nach einem Patronentausch: der Client-Stand gehoert
--- zur ALTEN Patrone und darf die neue nicht ueberschreiben
local function removeGear(outOfAir, immediateSync, ohneSync)
if not ohneSync then syncTank(immediateSync) end
currentGear.enabled = false
setLamp(false)
applyNarcosisStage(0)
activeTank = nil
warned = {}
narcosisWarned = false
narcosisDamageBuffer = 0.0
engineDepthWarned = false
disableScuba(outOfAir)
-- Stop breathing suit audio
end
--- Lässt den Spieler eine Flasche wählen. Bei nur einer Flasche entfällt das Menü.
--- @param tanks table[]
--- @return table? tank
local function chooseTank(tanks)
if #tanks == 1 then return tanks[1] end
local selection = promise.new()
local options = {}
for i = 1, #tanks do
local tank = tanks[i]
options[i] = {
title = tank.label,
description = locale('info.tank_remaining', formatTime(tank.oxygen),
math.floor(tank.oxygen / tank.capacity * 100),
tank.gas or '?', math.floor(tank.maxDepth or 0)),
icon = 'fa-solid fa-bottle-water',
onSelect = function() selection:resolve(tank) end
}
end
lib.registerContext({
id = 'd4rk_divegear_tanks',
title = locale('menu.choose_tank'),
options = options,
onExit = function() selection:resolve(nil) end
})
lib.showContext('d4rk_divegear_tanks')
return Citizen.Await(selection)
end
lib.callback.register('d4rk_divegear:client:fillTank', function(tanks)
if IsPedSwimmingUnderWater(cache.ped) then
lib.notify({ description = locale('error.underwater'), type = 'error' })
return
end
local tank = chooseTank(tanks)
if not tank then return end
if tank.oxygen >= tank.capacity then
lib.notify({ description = locale('error.tank_already_full'), type = 'error' })
return
end
if not exports.d4rk_lib:progress({
duration = config.refillTankTimeMs,
label = locale('info.filling_air'),
useWhileDead = false,
canCancel = true,
anim = {
dict = 'clothingshirt',
clip = 'try_shirt_positive_d',
blendIn = 8.0
}
}) then
return
end
-- Erfolgsmeldung und das Nachziehen der getragenen Flasche macht der Server,
-- sobald das Füll-Item tatsächlich entfernt wurde.
return tank.slot
end)
--- Getragene Flasche nach dem Auffüllen mitziehen, sonst überschreibt der
--- nächste Sync die frische Füllung wieder mit dem alten Client-Stand.
RegisterNetEvent('d4rk_divegear:client:tankRefilled', function(slot, capacity)
if not activeTank or activeTank.slot ~= slot then return end
activeTank.oxygen = capacity
warned = {}
enableScuba()
end)
--- Manometer-Anzeige. Bei konstantem Volumen ist der Druck proportional zur
--- verbliebenen Gasmenge, deshalb reicht der lineare Anteil an der Füllung.
--- @return number bar
local function getPressure()
if not activeTank or activeTank.capacity <= 0 then return 0 end
return (activeTank.pressure or 0) * (activeTank.oxygen / activeTank.capacity)
end
--- @param visible boolean
local function sendHudState(visible)
local oxygen = activeTank and activeTank.oxygen or 0.0
local capacity = activeTank and activeTank.capacity or 1
local pressure = getPressure()
local level = 'ok'
if oxygen <= dangerThreshold then
level = 'danger'
elseif oxygen <= warnThreshold then
level = 'warn'
end
SendNUIMessage({
action = 'state',
visible = visible,
oxygen = oxygen,
capacity = capacity,
depth = currentDepth,
-- Skalenwerte für die beiden Zeigerinstrumente. Kommen mit, damit die
-- Config an einer Stelle bleibt und das NUI nichts nachrechnen muss.
depthScale = config.depthDialMax,
depthLimit = activeTank and activeTank.maxDepth or nil,
pressure = pressure,
maxPressure = activeTank and activeTank.pressure or nil,
reservePressure = config.reservePressure,
reserve = pressure > 0 and pressure <= config.reservePressure,
level = level,
deep = currentDepth >= config.deepWarningDepth,
-- Grenze der getragenen Flasche überschritten: Tiefenrausch läuft.
overLimit = config.narcosis.enabled and getDepthOverLimit() >= 0,
hasLamp = true,
lamp = lampOn
})
end
--- Manuell ausgeblendet (eigene Taste oder Export)? Unabhängig davon, ob das
--- Spiel-HUD gerade sichtbar ist.
local hudHidden = false
--- Blendet das Tauch-HUD aus oder wieder ein, z.B. für Unterwasser-Screenshots.
--- Als Export nutzbar, damit HUD- und Screenshot-Resources es mitnehmen können:
--- exports.d4rk_divegear:setHudVisible(false)
--- @param visible boolean
local function setHudVisible(visible)
hudHidden = not visible
if hudHidden then
SendNUIMessage({ action = 'state', visible = false })
end
end
exports('setHudVisible', setHudVisible)
exports('isHudVisible', function() return not hudHidden end)
-- Fuer /tauchprobe beobachten (client/probe.lua): ist das Tauchgeraet aktiv?
exports('geraetAktiv', function() return currentGear.enabled == true end)
--- Pflegt die geglättete Tiefe und schiebt den Zustand ins NUI.
--- Läuft auch über Wasser weiter, weil getDecayFactor die Tiefe braucht - nur die
--- Anzeige wird ausgeblendet.
local function startHudThread()
CreateThread(function()
local wasVisible = false
SendNUIMessage({
action = 'labels',
scale = config.hudScale,
air = locale('hud.air'),
depth = locale('hud.depth'),
pressure = locale('hud.pressure'),
lamp = locale('hud.lamp')
})
local meinLauf = gearLauf
while currentGear.enabled and meinLauf == gearLauf do
currentDepth = currentDepth + (getDepth() - currentDepth) * 0.25
local visible = config.hudEnabled
and not hudHidden
and activeTank ~= nil
-- Standardmäßig schon an Land sichtbar, damit sich der
-- Flaschendruck ohne Inventar ablesen lässt.
and (not config.hudOnlyUnderwater or IsPedSwimmingUnderWater(cache.ped))
-- Folgt dem Spiel-HUD: alles was DisplayHud(false) setzt, blendet
-- damit auch das Tauch-HUD aus.
and not (config.hudFollowGameHud and IsHudHidden())
if visible or wasVisible then
sendHudState(visible)
wasVisible = visible
end
Wait(config.hudUpdateIntervalMs)
end
-- Abgeloest durch einen neuen Faden (Patronentausch): der zeigt weiter.
if meinLauf ~= gearLauf and currentGear.enabled then return end
currentDepth = 0.0
sendHudState(false)
end)
end
--- Wertet die Tiefe gegen die Grenze der getragenen Flasche aus und setzt Effekte,
--- Verbrauchsaufschlag und Schaden. Läuft auch über Wasser weiter, damit die
--- Effekte beim Auftauchen wieder verschwinden.
--- @param elapsed number Sekunden seit dem letzten Aufruf
local function updateNarcosis(elapsed)
if not config.narcosis.enabled then return end
-- Die Engine tötet ab einer bestimmten Tiefe über den Wasserdruck. Dagegen hilft
-- kein Atemgas, deshalb warnt das hier unabhängig von der Flasche.
local engineWarnAt = config.narcosis.engineDepthLimit - config.narcosis.engineWarnBeforeMeters
if currentDepth >= engineWarnAt then
if not engineDepthWarned then
engineDepthWarned = true
lib.notify({ description = locale('error.crush_depth', math.floor(config.narcosis.engineDepthLimit)), type = 'error' })
end
else
engineDepthWarned = false
end
local over = getDepthOverLimit()
local stages = config.narcosis.stages
local stage = 0
for i = 1, #stages do
if over >= stages[i].over then stage = i end
end
applyNarcosisStage(stage)
-- Vorwarnung, solange noch Zeit zum Umkehren ist.
if activeTank and over >= -config.narcosis.warnBeforeMeters then
if not narcosisWarned then
narcosisWarned = true
lib.notify({ description = locale('info.depth_limit', math.floor(activeTank.maxDepth or 0)), type = 'warning' })
end
else
narcosisWarned = false
end
local data = stage > 0 and stages[stage]
if not data or (data.damagePerTick or 0) <= 0 then
narcosisDamageBuffer = 0.0
return
end
-- Schaden sammeln statt jeden Tick zu runden, sonst geht bei 250 ms Takt
-- alles unter 4 Schaden pro Sekunde verloren.
narcosisDamageBuffer = narcosisDamageBuffer + data.damagePerTick * elapsed
if narcosisDamageBuffer >= 1.0 then
local damage = math.floor(narcosisDamageBuffer)
narcosisDamageBuffer -= damage
ApplyDamageToPed(cache.ped, damage, false)
end
end
--- Zieht Luft über die tatsächlich vergangene Zeit ab statt über gezählte Ticks.
--- Ein Wait(1000) läuft bei Last spürbar länger als eine Sekunde - über einen
--- 20-Minuten-Tauchgang summiert sich das sichtbar auf.
local function startOxygenLevelDecrementerThread()
CreateThread(function()
local lastTick = GetGameTimer()
local sinceSync = 0.0
local meinLauf = gearLauf
while currentGear.enabled and meinLauf == gearLauf do
Wait(250)
local now = GetGameTimer()
local elapsed = (now - lastTick) / 1000
lastTick = now
updateNarcosis(elapsed)
if not activeTank then goto continue end
if IsPedSwimmingUnderWater(cache.ped) and activeTank.oxygen > 0 then
activeTank.oxygen = math.max(0.0, activeTank.oxygen - elapsed * config.decayRate * getDecayFactor())
for i = 1, #config.warnAtSeconds do
local threshold = config.warnAtSeconds[i]
if activeTank.oxygen <= threshold and not warned[threshold] then
warned[threshold] = true
lib.notify({ description = locale('info.oxygen_low', formatTime(activeTank.oxygen)), type = 'warning' })
end
end
if activeTank.oxygen <= 0 then
disableScuba(true)
lib.notify({ description = locale('error.out_of_air'), type = 'error' })
syncTank()
sinceSync = 0.0
-- Stop breathing suit audio
goto continue
end
sinceSync = sinceSync + elapsed
if sinceSync >= shared.syncIntervalSeconds then
sinceSync = 0.0
syncTank()
end
end
::continue::
end
end)
end
--- Hängt die Ausrüstung nach, wenn sie verloren gegangen ist, und stellt die
--- Scuba-Flags wieder her. Nötig weil Fremd-Scripts (Emotes, Handcuffs, Death-Handler)
--- attached Props aufräumen und SetEnableScuba/SetPedMaxTimeUnderwater bei Respawn
--- und Fahrzeugausstieg vom Spiel zurückgesetzt werden.
local function startGearWatchdogThread()
CreateThread(function()
local meinLauf = gearLauf
while currentGear.enabled and meinLauf == gearLauf do
if config.removeGearOnDeath and IsPedDeadOrDying(cache.ped, true) then
removeGear(false, true)
break
end
if activeTank and activeTank.oxygen > 0 then
enableScuba()
end
Wait(config.gearWatchdogIntervalMs)
end
end)
end
--[[
ANLEGEN - 28.09.2026. Kein Balken und keine Props mehr: angezogen wird im
Inventar (Flasche und Maske sind Kleidung). Hier wird nur das Tauchgeraet
scharf geschaltet, wenn der Server sagt, dass beides getragen wird und
Luft in der Patrone ist.
]]
--- @param tank table aus d4rk_divegear:server:getWorn
--- @return boolean
local function anlegen(tank)
if not tank or (tank.oxygen or 0) <= 0 then return false end
activeTank = tank
warned = {}
gearLauf = gearLauf + 1
enableScuba()
currentGear.enabled = true
startOxygenLevelDecrementerThread()
startHudThread()
startGearWatchdogThread()
return true
end
--[[
NACHSEHEN, WAS ER TRAEGT - bei jeder Inventaraenderung und nach jedem
Umziehen. Mehrere Anstoesse in kurzer Folge fuehren zu EINER Frage mehr,
nicht zu vielen parallelen.
]]
local pruefenLaeuft, pruefenNochmal = false, false
local function pruefen()
if pruefenLaeuft then
pruefenNochmal = true
return
end
pruefenLaeuft = true
CreateThread(function()
repeat
pruefenNochmal = false
local erg = lib.callback.await('d4rk_divegear:server:getWorn', false)
if type(erg) == 'table' then
if erg.ok then
if not currentGear.enabled then
if anlegen(erg.tank) then
lib.notify({ description = locale('info.bereit', formatTime(erg.tank.oxygen)), type = 'info' })
end
elseif activeTank and activeTank.name ~= erg.tank.name then
-- Patrone getauscht: der Client-Stand gehoert zur alten.
removeGear(false, false, true)
anlegen(erg.tank)
end
elseif currentGear.enabled then
lib.notify({ description = locale('error.' .. (erg.grund or 'gear_gone')), type = 'error' })
removeGear(false)
end
end
until not pruefenNochmal
pruefenLaeuft = false
end)
end
AddEventHandler('d4rk:kleidung:angezogen', pruefen)
AddEventHandler('ox_inventory:updateInventory', pruefen)
CreateThread(function()
Wait(2000)
pruefen()
end)
--[[
EINE PATRONE BENUTZEN = SIE EINSETZEN. Nur an Land, mit Balken - wie
vorher das Anlegen.
UEBER EINEN CLIENT-EXPORT AM ITEM, NICHT CreateUseableItem (28.09.2026).
d4rk_items setzt consume immer (daten.lua:224, `or 1`), und d4rk_inventory
erreicht den qbx-Rueckruf nur bei consume == nil (server.lua:623) - fuer
Items aus unserer Tabelle feuerte er also nie. Die Patronen tragen
client_export = 'd4rk_divegear.patroneBenutzen' und consume = 0 (angelegt
mit /tauchartikel, server/artikel.lua); das Inventar ruft
exports.d4rk_divegear:patroneBenutzen(daten, slot) mit slot = { name,
slot, metadata } - dasselbe wie d4rk_medical `benutzen`.
]]
local function einsetzen(slot)
if IsPedSwimming(cache.ped) or cache.vehicle then
lib.notify({ description = locale('error.not_standing_up'), type = 'error' })
return
end
if not exports.d4rk_lib:progress({
duration = config.putOnSuitTimeMs,
label = locale('info.patrone_einsetzen'),
useWhileDead = false,
canCancel = true,
anim = {
dict = 'clothingshirt',
clip = 'try_shirt_positive_d',
blendIn = 8.0
}
}) then
return
end
-- Die laufende Luft zuerst sichern - danach gehoert der Client-Stand zur
-- alten Patrone und wird nicht mehr geschrieben (pruefen -> removeGear ohneSync).
if currentGear.enabled then syncTank() end
local ok, grund = lib.callback.await('d4rk_divegear:server:patroneTauschen', false, slot)
if not ok then
lib.notify({ description = locale('error.' .. tostring(grund or 'fehler')), type = 'error' })
return
end
lib.notify({ description = locale('success.patrone_getauscht'), type = 'success' })
--[[
IMMER NEU EINLESEN - 29.09.2026. Betreiber: "beim austauschen
restettet sich das hud aber nicht". pruefen() las die Patrone nur neu,
wenn sich ihr NAME aenderte - eine 5-Minuten-Patrone gegen eine andere
5-Minuten-Patrone galt als dieselbe, und die alte Restluft blieb stehen.
Die laufende Luft ist oben schon gesichert (syncTank vor dem Tausch),
also abbauen OHNE zu schreiben und frisch vom Server anlegen.
]]
if currentGear.enabled then removeGear(false, false, true) end
pruefen()
end
exports('patroneBenutzen', function(_, slot)
einsetzen(type(slot) == 'table' and slot.slot or nil)
end)
lib.addKeybind({
name = 'divegear_hud',
description = locale('keybind.toggle_hud'),
defaultKey = config.hudToggleKey,
onPressed = function()
setHudVisible(hudHidden)
end
})
lib.addKeybind({
name = 'divegear_lamp',
description = locale('keybind.toggle_lamp'),
defaultKey = config.lampKey,
onPressed = function()
if not currentGear.enabled then return end
setLamp(not lampOn, true)
end
})
--- Beim Model-Wechsel (Kleiderladen, Skin-Change) ist der Ped ein anderer -
--- alle attached Props sind weg und die Scuba-Flags gelten für den alten Ped.
---
--- Der neue Ped kommt zwingend aus dem Callback-Parameter: ox_lib feuert die
--- onCache-Callbacks über Citizen.CreateThreadNow, *bevor* der neue Wert in den
--- Cache geschrieben wird (ox_lib/init.lua). cache.ped zeigt hier also noch auf
--- den alten, bereits verschwundenen Ped.
lib.onCache('ped', function(ped)
if not currentGear.enabled then return end
if activeTank and activeTank.oxygen > 0 then
enableScuba(ped)
end
end)
--[[
GTA NIMMT FLASCHE UND MASKE SELBST AB - gemessen am 29.09.2026 mit
/tauchprobe beobachten (Betreiber): im Moment, in dem "schwimmt" auf nein
springt (flaches Wasser, Boden unter den Fuessen), verschwanden Komponente 8
(global 123 = GTAs Taucherflasche) und Prop 1 gleichzeitig - bei
ausgeschaltetem Tauchgeraet, also nicht durch diesen Code. Die Items lagen
weiter in ihren Slots.
CPED_CONFIG_FLAG_DisableTakeOffScubaGear = 409 (NACHGESCHLAGEN in der
SetPedConfigFlag-Doku der Nativ-Datenbank) schaltet das ab. Bei uns zieht
nur das Inventar aus. Gesetzt am Spieler-Ped, nach jedem Ped-Wechsel neu
(onCache-Parameter, nicht cache.ped), beim Stoppen wieder ab - sonst
bliebe sie ohne diese Resource stehen.
]]
local ABNEHMEN_AUS = 409
CreateThread(function()
SetPedConfigFlag(PlayerPedId(), ABNEHMEN_AUS, true)
end)
lib.onCache('ped', function(ped)
SetPedConfigFlag(ped, ABNEHMEN_AUS, true)
end)
AddEventHandler('onResourceStop', function(resource)
if resource ~= GetCurrentResourceName() then return end
SetPedConfigFlag(cache.ped, ABNEHMEN_AUS, false)
syncTank(true)
setLamp(false)
applyNarcosisStage(0)
SendNUIMessage({ action = 'state', visible = false })
SetEnableScuba(cache.ped, false)
SetPedMaxTimeUnderwater(cache.ped, config.maxTimeUnderwater)
end)