From d661152bc95c2b1a514fdcc438929bd01fc44b55 Mon Sep 17 00:00:00 2001 From: D4rkst3r Date: Mon, 10 Aug 2026 23:45:11 +0200 Subject: [PATCH] feat: Flaschengroessen mit echter Laufzeit und persistenter Restluft Der Sauerstoff war eine lokale Client-Variable: nicht persistent, an kein Item gebunden, weg bei Relog. Dazu ergab startingOxygenLevel=100 bei decayRate=1/s genau 100 Sekunden Tauchzeit statt Minuten. Sauerstoff-Logik: - Restsekunden statt Punkte, Verbrauch ueber GetGameTimer-Delta statt gezaehlter Wait(1000) - ein Tick laeuft unter Last laenger als eine Sekunde, was sich ueber einen langen Tauchgang aufsummiert - Float-Vergleiche (oxygenLevel % 10 == 0, == 0) raus, Warnschwellen als config.warnAtSeconds, jede Schwelle einmal pro Tauchgang - Verbrauch steigt mit der Tiefe (1 + tiefe/30, gedeckelt), abschaltbar - Anzeige als mm:ss Flaschen: - Vier Items mit capacity in Sekunden (300/600/900/1200) in config/shared.lua - Restluft in metadata.oxygen, Fuellstand zusaetzlich in metadata.durability, damit der Balken direkt im Inventar-Slot sichtbar ist - Kein decay auf den Items: ox_inventory loescht sonst die leere Flasche - Auswahlmenue beim Anlegen und Auffuellen (entfaellt bei nur einer Flasche) - diving_fill fuellt bis capacity statt auf einen Fixwert, verweigert volle Flaschen und verbraucht sich nur bei Erfolg - items_for_ox_inventory.lua zum Reinkopieren Server: - getTanks/syncTank als Callbacks, Restluft kann nur sinken und nicht schneller als maxDrainPerSecond pro echter Sekunde - SetMetadata ersetzt die Metadata-Tabelle komplett, daher wird der bestehende Inhalt kopiert statt nur oxygen gesetzt Co-Authored-By: Claude Opus 5 --- client/main.lua | 208 ++++++++++++++++++++++++++++++++----- config/client.lua | 23 ++-- config/shared.lua | 34 ++++++ fxmanifest.lua | 1 + items_for_ox_inventory.lua | 69 ++++++++++++ locales/de.json | 17 ++- locales/en.json | 17 ++- server/main.lua | 147 +++++++++++++++++++++++++- 8 files changed, 469 insertions(+), 47 deletions(-) create mode 100644 config/shared.lua create mode 100644 items_for_ox_inventory.lua diff --git a/client/main.lua b/client/main.lua index 324f130..90e73c1 100644 --- a/client/main.lua +++ b/client/main.lua @@ -1,4 +1,5 @@ local config = require 'config.client' +local shared = require 'config.shared' local currentGear = { mask = 0, @@ -6,13 +7,42 @@ local currentGear = { enabled = false } -local oxygenLevel = 0 +--- 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 = {} local MASK_MODEL = `p_d_scuba_mask_s` local TANK_MODEL = `p_s_scuba_tank_s` local MASK_BONE = 12844 local TANK_BONE = 24818 +--- @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 + +--- Verbrauchsfaktor. Tiefer tauchen kostet mehr Luft. +local function getDecayFactor() + if not config.depthDecayEnabled then return 1.0 end + + return math.min(config.maxDecayFactor, 1.0 + getDepth() / config.depthDecayReference) +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 @@ -80,22 +110,87 @@ local function isGearIntact() return isPropIntact(currentGear.mask) and isPropIntact(currentGear.tank) 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? -local function removeGear(outOfAir) +--- @param immediateSync boolean? +local function removeGear(outOfAir, immediateSync) + syncTank(immediateSync) + currentGear.enabled = false + activeTank = nil + warned = {} + deleteGear() disableScuba(outOfAir) -- Stop breathing suit audio end -lib.callback.register('qbx_divegear:client:fillTank', function() - if IsPedSwimmingUnderWater(cache.ped) then - exports.qbx_core:Notify(locale('error.underwater', {oxygenlevel = oxygenLevel}), 'error') - return false +--- 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)), + icon = 'fa-solid fa-bottle-water', + onSelect = function() selection:resolve(tank) end + } end - if lib.progressBar({ + 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 + exports.qbx_core:Notify(locale('error.underwater'), 'error') + return + end + + local tank = chooseTank(tanks) + + if not tank then return end + + if tank.oxygen >= tank.capacity then + exports.qbx_core:Notify(locale('error.tank_already_full'), 'error') + return + end + + if not lib.progressBar({ duration = config.refillTankTimeMs, label = locale('info.filling_air'), useWhileDead = false, @@ -106,13 +201,22 @@ lib.callback.register('qbx_divegear:client:fillTank', function() blendIn = 8.0 } }) then - oxygenLevel = config.startingOxygenLevel - exports.qbx_core:Notify(locale('success.tube_filled'), 'success') - if currentGear.enabled then - enableScuba() - end - return true + 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) local function takeOffSuit() @@ -135,9 +239,9 @@ end local function startOxygenLevelDrawTextThread() CreateThread(function() while currentGear.enabled do - if IsPedSwimmingUnderWater(cache.ped) then + if IsPedSwimmingUnderWater(cache.ped) and activeTank then qbx.drawText2d({ - text = oxygenLevel..'⏱', + text = formatTime(activeTank.oxygen)..'⏱', coords = vec2(1.0, 1.42), scale = 0.45 }) @@ -147,20 +251,53 @@ local function startOxygenLevelDrawTextThread() 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 + while currentGear.enabled do - if IsPedSwimmingUnderWater(cache.ped) and oxygenLevel > 0 then - oxygenLevel -= config.decayRate - if oxygenLevel % 10 == 0 and oxygenLevel ~= config.startingOxygenLevel then - -- Initiate breathing suit audio + Wait(250) + + local now = GetGameTimer() + local elapsed = (now - lastTick) / 1000 + lastTick = now + + 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 + exports.qbx_core:Notify(locale('info.oxygen_low', formatTime(activeTank.oxygen)), 'warning') + end end - if oxygenLevel == 0 then + + if activeTank.oxygen <= 0 then disableScuba(true) + exports.qbx_core:Notify(locale('error.out_of_air'), '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 - Wait(1000) + + ::continue:: end end) end @@ -173,7 +310,7 @@ local function startGearWatchdogThread() CreateThread(function() while currentGear.enabled do if config.removeGearOnDeath and IsPedDeadOrDying(cache.ped, true) then - removeGear() + removeGear(false, true) break end @@ -182,7 +319,7 @@ local function startGearWatchdogThread() attachGear() end - if oxygenLevel > 0 then + if activeTank and activeTank.oxygen > 0 then enableScuba() end @@ -192,13 +329,24 @@ local function startGearWatchdogThread() end local function putOnSuit() - if oxygenLevel <= 0 then + if IsPedSwimming(cache.ped) or cache.vehicle then + exports.qbx_core:Notify(locale('error.not_standing_up'), 'error') + return + end + + local tanks = lib.callback.await('d4rk_divegear:server:getTanks', false) + + if not tanks or #tanks == 0 then exports.qbx_core:Notify(locale('error.need_otube'), 'error') return end - if IsPedSwimming(cache.ped) or cache.vehicle then - exports.qbx_core:Notify(locale('error.not_standing_up'), 'error') + local tank = chooseTank(tanks) + + if not tank then return end + + if tank.oxygen <= 0 then + exports.qbx_core:Notify(locale('error.tank_empty'), 'error') return end @@ -213,6 +361,9 @@ local function putOnSuit() blendIn = 8.0 } }) then + activeTank = tank + warned = {} + deleteGear() attachGear() enableScuba() @@ -224,7 +375,7 @@ local function putOnSuit() end end -RegisterNetEvent('qbx_divegear:client:useGear', function() +RegisterNetEvent('d4rk_divegear:client:useGear', function() if currentGear.enabled then takeOffSuit() else @@ -245,7 +396,7 @@ lib.onCache('ped', function(ped) deleteGear() attachGear(ped) - if oxygenLevel > 0 then + if activeTank and activeTank.oxygen > 0 then enableScuba(ped) end end) @@ -253,6 +404,7 @@ end) AddEventHandler('onResourceStop', function(resource) if resource ~= GetCurrentResourceName() then return end + syncTank(true) deleteGear() SetEnableScuba(cache.ped, false) SetPedMaxTimeUnderwater(cache.ped, config.maxTimeUnderwater) diff --git a/config/client.lua b/config/client.lua index b06f7e1..5f1e7ef 100644 --- a/config/client.lua +++ b/config/client.lua @@ -1,19 +1,30 @@ return { - startingOxygenLevel = 100, putOnSuitTimeMs = 5000, takeOffSuitTimeMs = 5000, refillTankTimeMs = 5000, - decayRate = 1, -- The rate at which the oxygen level decays. Defaults to 1. - -- Intervall des Watchdogs, der prüft ob die Ausrüstung noch am Ped hängt (ms). + --- Sekunden Luft, die pro echter Sekunde verbraucht werden (vor dem Tiefenfaktor). + decayRate = 1.0, + + --- Intervall des Watchdogs, der prüft ob die Ausrüstung noch am Ped hängt (ms). gearWatchdogIntervalMs = 1000, - -- Sekunden, die der Spieler ohne Ausrüstung unter Wasser überlebt (Vanilla-Verhalten). + --- Sekunden, die der Spieler ohne Ausrüstung unter Wasser überlebt (Vanilla-Verhalten). maxTimeUnderwater = 50.0, - -- Sekunden, sobald die Flasche leer ist. Bewusst kurz: leer tauchen soll wehtun. + --- Sekunden, sobald die Flasche leer ist. Bewusst kurz: leer tauchen soll wehtun. maxTimeUnderwaterOutOfAir = 1.0, - -- Ausrüstung beim Tod abnehmen. Die Flasche bleibt mit ihrer Restluft im Inventar. + --- Ausrüstung beim Tod abnehmen. Die Flasche bleibt mit ihrer Restluft im Inventar. removeGearOnDeath = true, + + --- Restsekunden, bei denen gewarnt wird. Jede Schwelle löst einmal pro Tauchgang aus. + warnAtSeconds = { 60, 30, 10 }, + + --- Verbrauch steigt mit der Tiefe: faktor = 1 + tiefe / depthDecayReference. + --- Macht die großen Flaschen erst sinnvoll. Auf false = konstanter Verbrauch, + --- dann stimmen die 5/10/15/20 Minuten exakt. + depthDecayEnabled = true, + depthDecayReference = 30.0, + maxDecayFactor = 4.0, } diff --git a/config/shared.lua b/config/shared.lua new file mode 100644 index 0000000..fee32fd --- /dev/null +++ b/config/shared.lua @@ -0,0 +1,34 @@ +-- Von Client und Server benutzt. Muss in fxmanifest unter files{} stehen, +-- damit lib.require die Datei auf beiden Seiten laden kann. + +local config = { + --- Tauchflaschen. capacity = Laufzeit in Sekunden bei normalem Verbrauch. + --- Reihenfolge = Reihenfolge im Auswahlmenü. + tanks = { + { name = 'diving_tank_small', capacity = 300 }, -- 5 Minuten + { name = 'diving_tank_medium', capacity = 600 }, -- 10 Minuten + { name = 'diving_tank_large', capacity = 900 }, -- 15 Minuten + { name = 'diving_tank_xl', capacity = 1200 }, -- 20 Minuten + }, + + --- Wie oft der Client die Restluft an den Server meldet (Sekunden). + --- Bei einem Crash gehen maximal so viele Sekunden Luft verloren. + syncIntervalSeconds = 15, + + --- Obergrenze für die serverseitige Plausibilitätsprüfung: so viele Sekunden + --- Luft kann ein Spieler höchstens pro echter Sekunde verbrauchen. Muss über + --- maxDecayFactor aus config/client.lua liegen, sonst wird ehrlichen Spielern + --- in großer Tiefe fälschlich Luft zurückgegeben. + maxDrainPerSecond = 5.0, +} + +config.tankByName = {} +config.tankNames = {} + +for i = 1, #config.tanks do + local tank = config.tanks[i] + config.tankByName[tank.name] = tank + config.tankNames[i] = tank.name +end + +return config diff --git a/fxmanifest.lua b/fxmanifest.lua index 1e6d561..186e005 100644 --- a/fxmanifest.lua +++ b/fxmanifest.lua @@ -19,6 +19,7 @@ client_script 'client/main.lua' files { 'config/client.lua', + 'config/shared.lua', 'locales/*.json', } diff --git a/items_for_ox_inventory.lua b/items_for_ox_inventory.lua new file mode 100644 index 0000000..199c7c4 --- /dev/null +++ b/items_for_ox_inventory.lua @@ -0,0 +1,69 @@ +-- Zum Reinkopieren in ox_inventory/data/items.lua +-- +-- WICHTIG: kein `decay = true` auf den Flaschen. Die durability trägt hier den +-- Füllstand, und ox_inventory löscht Items mit decay, sobald durability 0 erreicht +-- (modules/items/server.lua) - die leere Flasche würde also verschwinden statt +-- auffüllbar zu bleiben. +-- +-- Die Laufzeiten stehen in config/shared.lua (capacity in Sekunden). Wer die dort +-- ändert, sollte die Labels hier mitziehen. + +['diving_gear'] = { + label = 'Tauchausrüstung', + weight = 5000, + stack = false, + close = true, + consume = 0, + description = 'Maske und Tauchanzug. Benutzen zum An- und Ausziehen.', + client = { image = 'diving_gear.png' } +}, + +['diving_fill'] = { + label = 'Pressluft-Kartusche', + weight = 1000, + stack = true, + close = true, + consume = 0, + description = 'Füllt eine Tauchflasche wieder auf.', + client = { image = 'diving_fill.png' } +}, + +['diving_tank_small'] = { + label = 'Tauchflasche (5 Min)', + weight = 4000, + stack = false, + close = true, + consume = 0, + description = 'Kleine Tauchflasche. Reicht für etwa 5 Minuten.', + client = { image = 'diving_tank_small.png' } +}, + +['diving_tank_medium'] = { + label = 'Tauchflasche (10 Min)', + weight = 7000, + stack = false, + close = true, + consume = 0, + description = 'Mittlere Tauchflasche. Reicht für etwa 10 Minuten.', + client = { image = 'diving_tank_medium.png' } +}, + +['diving_tank_large'] = { + label = 'Tauchflasche (15 Min)', + weight = 10000, + stack = false, + close = true, + consume = 0, + description = 'Große Tauchflasche. Reicht für etwa 15 Minuten.', + client = { image = 'diving_tank_large.png' } +}, + +['diving_tank_xl'] = { + label = 'Tauchflasche (20 Min)', + weight = 13000, + stack = false, + close = true, + consume = 0, + description = 'Doppelflasche für lange Tauchgänge. Reicht für etwa 20 Minuten.', + client = { image = 'diving_tank_xl.png' } +}, diff --git a/locales/de.json b/locales/de.json index b6c74eb..e11a0c7 100644 --- a/locales/de.json +++ b/locales/de.json @@ -1,16 +1,25 @@ { "error": { "not_standing_up": "Du musst auf festem Boden stehen, um das anzulegen...", - "need_otube": "Du musst deinen Sauerstoff auffüllen! Hol dir eine neue Sauerstoffflasche!", - "underwater": "Du kannst das nicht unter Wasser tun..." + "need_otube": "Du hast keine Tauchflasche dabei!", + "underwater": "Du kannst das nicht unter Wasser tun...", + "no_tank": "Du hast keine Tauchflasche zum Auffüllen dabei!", + "tank_empty": "Diese Tauchflasche ist leer!", + "tank_already_full": "Diese Tauchflasche ist bereits voll!", + "out_of_air": "Deine Luft ist alle! Sofort auftauchen!" }, "success": { "took_out": "Du hast deine Ausrüstung abgelegt!", - "tube_filled": "Du hast deinen Sauerstofftank erfolgreich aufgefüllt!" + "tube_filled": "Du hast deine Tauchflasche erfolgreich aufgefüllt!" }, "info": { "put_suit": "Tauchanzug wird angezogen...", "pullout_suit": "Tauchanzug wird ausgezogen...", - "filling_air": "Sauerstoff wird aufgefüllt..." + "filling_air": "Sauerstoff wird aufgefüllt...", + "oxygen_low": "Nur noch %s Luft!", + "tank_remaining": "%s verbleibend (%d%%)" + }, + "menu": { + "choose_tank": "Tauchflasche wählen" } } diff --git a/locales/en.json b/locales/en.json index 329b26e..a8d1c43 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1,8 +1,12 @@ { "error": { "not_standing_up": "You need to be on solid ground to put this on...", - "need_otube": "You need to refill your oxygen! Get a replacement air supply!", - "underwater": "You cannot do this underwater..." + "need_otube": "You don't have an air tank with you!", + "underwater": "You cannot do this underwater...", + "no_tank": "You don't have an air tank to refill!", + "tank_empty": "This air tank is empty!", + "tank_already_full": "This air tank is already full!", + "out_of_air": "You're out of air! Surface now!" }, "success": { "took_out": "You've taken your gear off!", @@ -11,6 +15,11 @@ "info": { "put_suit": "Putting on your diving suit...", "pullout_suit": "Taking off your diving suit...", - "filling_air": "Filling air..." + "filling_air": "Filling air...", + "oxygen_low": "Only %s of air left!", + "tank_remaining": "%s remaining (%d%%)" + }, + "menu": { + "choose_tank": "Choose an air tank" } -} \ No newline at end of file +} diff --git a/server/main.lua b/server/main.lua index e288cb1..ff63a24 100644 --- a/server/main.lua +++ b/server/main.lua @@ -1,10 +1,147 @@ +local shared = require 'config.shared' + +--- Zeitpunkt des letzten akzeptierten Syncs pro Spieler. Basis für die +--- Plausibilitätsprüfung: mehr als maxDrainPerSecond pro echter Sekunde kann +--- niemand verbrauchen. +local lastSync = {} + +--- @param source number +--- @param slotId number +--- @return table? slot, table? tankConfig +local function getTankSlot(source, slotId) + if type(slotId) ~= 'number' then return end + + local slot = exports.ox_inventory:GetSlot(source, slotId) + + if not slot then return end + + local tankConfig = shared.tankByName[slot.name] + + if not tankConfig then return end + + return slot, tankConfig +end + +--- Schreibt die Restluft in die Metadata. +--- SetMetadata *ersetzt* die Metadata-Tabelle komplett (modules/inventory/server.lua), +--- deshalb wird der bestehende Inhalt kopiert statt nur oxygen gesetzt. +--- @return number oxygen akzeptierte Restsekunden +local function writeTank(source, slot, tankConfig, oxygen) + oxygen = math.floor(math.max(0, math.min(oxygen, tankConfig.capacity))) + + local metadata = {} + + for key, value in pairs(slot.metadata or {}) do + metadata[key] = value + end + + metadata.oxygen = oxygen + -- durability zeigt den Füllstand direkt im Inventar-Slot an. Kein decay auf den + -- Items, sonst würde ox_inventory die leere Flasche löschen (items/server.lua). + metadata.durability = math.floor(oxygen / tankConfig.capacity * 100) + + exports.ox_inventory:SetMetadata(source, slot.slot, metadata) + + return oxygen +end + +--- Alle Tauchflaschen im Inventar mit Füllstand. +--- @param source number +--- @return table[] +local function getTankList(source) + local found = exports.ox_inventory:Search(source, 'slots', shared.tankNames) + local tanks = {} + + for i = 1, #shared.tankNames do + local name = shared.tankNames[i] + local slots = found and found[name] + local tankConfig = shared.tankByName[name] + + if slots then + for j = 1, #slots do + local slot = slots[j] + local oxygen = tonumber(slot.metadata and slot.metadata.oxygen) or 0 + + tanks[#tanks + 1] = { + slot = slot.slot, + name = name, + label = (exports.ox_inventory:Items(name) or {}).label or name, + oxygen = math.max(0, math.min(oxygen, tankConfig.capacity)), + capacity = tankConfig.capacity + } + end + end + end + + return tanks +end + +--- @return number? accepted +local function syncTank(source, slotId, oxygen) + oxygen = tonumber(oxygen) + + if not oxygen then return end + + local slot, tankConfig = getTankSlot(source, slotId) + + if not slot then return end + + local current = tonumber(slot.metadata and slot.metadata.oxygen) or tankConfig.capacity + local now = os.time() + local elapsed = lastSync[source] and (now - lastSync[source]) or shared.syncIntervalSeconds + + lastSync[source] = now + + -- Luft kann nur weniger werden, und nicht schneller als physikalisch möglich. + local floorValue = math.max(0, current - math.max(1, elapsed) * shared.maxDrainPerSecond) + local accepted = math.max(floorValue, math.min(oxygen, current)) + + return writeTank(source, slot, tankConfig, accepted) +end + +lib.callback.register('d4rk_divegear:server:getTanks', getTankList) + +lib.callback.register('d4rk_divegear:server:syncTank', function(source, slotId, oxygen) + return syncTank(source, slotId, oxygen) +end) + +--- Fire-and-Forget-Variante für Tod und Resource-Stop, wo auf keine Antwort +--- mehr gewartet werden kann. +RegisterNetEvent('d4rk_divegear:server:syncTankNow', function(slotId, oxygen) + syncTank(source, slotId, oxygen) +end) + +AddEventHandler('playerDropped', function() + lastSync[source] = nil +end) + exports.qbx_core:CreateUseableItem('diving_gear', function(source) - TriggerClientEvent('qbx_divegear:client:useGear', source) + TriggerClientEvent('d4rk_divegear:client:useGear', source) end) exports.qbx_core:CreateUseableItem('diving_fill', function(source) - local success = lib.callback.await('qbx_divegear:client:fillTank', source) - if success then - exports.ox_inventory:RemoveItem(source, 'diving_fill', 1) + local tanks = getTankList(source) + + if #tanks == 0 then + exports.qbx_core:Notify(source, locale('error.no_tank'), 'error') + return end -end) \ No newline at end of file + + local slotId = lib.callback.await('d4rk_divegear:client:fillTank', source, tanks) + + if not slotId then return end + + local slot, tankConfig = getTankSlot(source, slotId) + + if not slot then return end + + -- Nach dem Progressbar nochmal prüfen: das Item kann in der Zwischenzeit weg sein. + if not exports.ox_inventory:RemoveItem(source, 'diving_fill', 1) then return end + + writeTank(source, slot, tankConfig, tankConfig.capacity) + lastSync[source] = os.time() + + -- Erst melden wenn das Item tatsächlich weg und die Flasche voll ist. + exports.qbx_core:Notify(source, locale('success.tube_filled'), 'success') + TriggerClientEvent('d4rk_divegear:client:tankRefilled', source, slot.slot, tankConfig.capacity) +end)