feat: Flaschengroessen mit echter Laufzeit und persistenter Restluft
Lint / Lint Resource (push) Has been cancelled

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 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 23:45:11 +02:00
co-authored by Claude Opus 5
parent 58f31adfa9
commit d661152bc9
8 changed files with 469 additions and 47 deletions
+180 -28
View File
@@ -1,4 +1,5 @@
local config = require 'config.client' local config = require 'config.client'
local shared = require 'config.shared'
local currentGear = { local currentGear = {
mask = 0, mask = 0,
@@ -6,13 +7,42 @@ local currentGear = {
enabled = false 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 MASK_MODEL = `p_d_scuba_mask_s`
local TANK_MODEL = `p_s_scuba_tank_s` local TANK_MODEL = `p_s_scuba_tank_s`
local MASK_BONE = 12844 local MASK_BONE = 12844
local TANK_BONE = 24818 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 --- @param ped number? default cache.ped - beim Model-Wechsel muss der neue Ped explizit rein
local function enableScuba(ped) local function enableScuba(ped)
ped = ped or cache.ped ped = ped or cache.ped
@@ -80,22 +110,87 @@ local function isGearIntact()
return isPropIntact(currentGear.mask) and isPropIntact(currentGear.tank) return isPropIntact(currentGear.mask) and isPropIntact(currentGear.tank)
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. --- Gemeinsamer Teardown für Ablegen, leere Flasche und Tod.
--- @param outOfAir boolean? --- @param outOfAir boolean?
local function removeGear(outOfAir) --- @param immediateSync boolean?
local function removeGear(outOfAir, immediateSync)
syncTank(immediateSync)
currentGear.enabled = false currentGear.enabled = false
activeTank = nil
warned = {}
deleteGear() deleteGear()
disableScuba(outOfAir) disableScuba(outOfAir)
-- Stop breathing suit audio -- Stop breathing suit audio
end end
lib.callback.register('qbx_divegear:client:fillTank', function() --- Lässt den Spieler eine Flasche wählen. Bei nur einer Flasche entfällt das Menü.
if IsPedSwimmingUnderWater(cache.ped) then --- @param tanks table[]
exports.qbx_core:Notify(locale('error.underwater', {oxygenlevel = oxygenLevel}), 'error') --- @return table? tank
return false 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 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, duration = config.refillTankTimeMs,
label = locale('info.filling_air'), label = locale('info.filling_air'),
useWhileDead = false, useWhileDead = false,
@@ -106,13 +201,22 @@ lib.callback.register('qbx_divegear:client:fillTank', function()
blendIn = 8.0 blendIn = 8.0
} }
}) then }) then
oxygenLevel = config.startingOxygenLevel return
exports.qbx_core:Notify(locale('success.tube_filled'), 'success')
if currentGear.enabled then
enableScuba()
end
return true
end 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) end)
local function takeOffSuit() local function takeOffSuit()
@@ -135,9 +239,9 @@ end
local function startOxygenLevelDrawTextThread() local function startOxygenLevelDrawTextThread()
CreateThread(function() CreateThread(function()
while currentGear.enabled do while currentGear.enabled do
if IsPedSwimmingUnderWater(cache.ped) then if IsPedSwimmingUnderWater(cache.ped) and activeTank then
qbx.drawText2d({ qbx.drawText2d({
text = oxygenLevel..'', text = formatTime(activeTank.oxygen)..'',
coords = vec2(1.0, 1.42), coords = vec2(1.0, 1.42),
scale = 0.45 scale = 0.45
}) })
@@ -147,20 +251,53 @@ local function startOxygenLevelDrawTextThread()
end) end)
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() local function startOxygenLevelDecrementerThread()
CreateThread(function() CreateThread(function()
local lastTick = GetGameTimer()
local sinceSync = 0.0
while currentGear.enabled do while currentGear.enabled do
if IsPedSwimmingUnderWater(cache.ped) and oxygenLevel > 0 then Wait(250)
oxygenLevel -= config.decayRate
if oxygenLevel % 10 == 0 and oxygenLevel ~= config.startingOxygenLevel then local now = GetGameTimer()
-- Initiate breathing suit audio 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 end
if oxygenLevel == 0 then
if activeTank.oxygen <= 0 then
disableScuba(true) disableScuba(true)
exports.qbx_core:Notify(locale('error.out_of_air'), 'error')
syncTank()
sinceSync = 0.0
-- Stop breathing suit audio -- Stop breathing suit audio
goto continue
end
sinceSync = sinceSync + elapsed
if sinceSync >= shared.syncIntervalSeconds then
sinceSync = 0.0
syncTank()
end end
end end
Wait(1000)
::continue::
end end
end) end)
end end
@@ -173,7 +310,7 @@ local function startGearWatchdogThread()
CreateThread(function() CreateThread(function()
while currentGear.enabled do while currentGear.enabled do
if config.removeGearOnDeath and IsPedDeadOrDying(cache.ped, true) then if config.removeGearOnDeath and IsPedDeadOrDying(cache.ped, true) then
removeGear() removeGear(false, true)
break break
end end
@@ -182,7 +319,7 @@ local function startGearWatchdogThread()
attachGear() attachGear()
end end
if oxygenLevel > 0 then if activeTank and activeTank.oxygen > 0 then
enableScuba() enableScuba()
end end
@@ -192,13 +329,24 @@ local function startGearWatchdogThread()
end end
local function putOnSuit() 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') exports.qbx_core:Notify(locale('error.need_otube'), 'error')
return return
end end
if IsPedSwimming(cache.ped) or cache.vehicle then local tank = chooseTank(tanks)
exports.qbx_core:Notify(locale('error.not_standing_up'), 'error')
if not tank then return end
if tank.oxygen <= 0 then
exports.qbx_core:Notify(locale('error.tank_empty'), 'error')
return return
end end
@@ -213,6 +361,9 @@ local function putOnSuit()
blendIn = 8.0 blendIn = 8.0
} }
}) then }) then
activeTank = tank
warned = {}
deleteGear() deleteGear()
attachGear() attachGear()
enableScuba() enableScuba()
@@ -224,7 +375,7 @@ local function putOnSuit()
end end
end end
RegisterNetEvent('qbx_divegear:client:useGear', function() RegisterNetEvent('d4rk_divegear:client:useGear', function()
if currentGear.enabled then if currentGear.enabled then
takeOffSuit() takeOffSuit()
else else
@@ -245,7 +396,7 @@ lib.onCache('ped', function(ped)
deleteGear() deleteGear()
attachGear(ped) attachGear(ped)
if oxygenLevel > 0 then if activeTank and activeTank.oxygen > 0 then
enableScuba(ped) enableScuba(ped)
end end
end) end)
@@ -253,6 +404,7 @@ end)
AddEventHandler('onResourceStop', function(resource) AddEventHandler('onResourceStop', function(resource)
if resource ~= GetCurrentResourceName() then return end if resource ~= GetCurrentResourceName() then return end
syncTank(true)
deleteGear() deleteGear()
SetEnableScuba(cache.ped, false) SetEnableScuba(cache.ped, false)
SetPedMaxTimeUnderwater(cache.ped, config.maxTimeUnderwater) SetPedMaxTimeUnderwater(cache.ped, config.maxTimeUnderwater)
+17 -6
View File
@@ -1,19 +1,30 @@
return { return {
startingOxygenLevel = 100,
putOnSuitTimeMs = 5000, putOnSuitTimeMs = 5000,
takeOffSuitTimeMs = 5000, takeOffSuitTimeMs = 5000,
refillTankTimeMs = 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, 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, 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, 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, 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,
} }
+34
View File
@@ -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
+1
View File
@@ -19,6 +19,7 @@ client_script 'client/main.lua'
files { files {
'config/client.lua', 'config/client.lua',
'config/shared.lua',
'locales/*.json', 'locales/*.json',
} }
+69
View File
@@ -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' }
},
+13 -4
View File
@@ -1,16 +1,25 @@
{ {
"error": { "error": {
"not_standing_up": "Du musst auf festem Boden stehen, um das anzulegen...", "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!", "need_otube": "Du hast keine Tauchflasche dabei!",
"underwater": "Du kannst das nicht unter Wasser tun..." "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": { "success": {
"took_out": "Du hast deine Ausrüstung abgelegt!", "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": { "info": {
"put_suit": "Tauchanzug wird angezogen...", "put_suit": "Tauchanzug wird angezogen...",
"pullout_suit": "Tauchanzug wird ausgezogen...", "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"
} }
} }
+12 -3
View File
@@ -1,8 +1,12 @@
{ {
"error": { "error": {
"not_standing_up": "You need to be on solid ground to put this on...", "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!", "need_otube": "You don't have an air tank with you!",
"underwater": "You cannot do this underwater..." "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": { "success": {
"took_out": "You've taken your gear off!", "took_out": "You've taken your gear off!",
@@ -11,6 +15,11 @@
"info": { "info": {
"put_suit": "Putting on your diving suit...", "put_suit": "Putting on your diving suit...",
"pullout_suit": "Taking off 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"
} }
} }
+141 -4
View File
@@ -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) exports.qbx_core:CreateUseableItem('diving_gear', function(source)
TriggerClientEvent('qbx_divegear:client:useGear', source) TriggerClientEvent('d4rk_divegear:client:useGear', source)
end) end)
exports.qbx_core:CreateUseableItem('diving_fill', function(source) exports.qbx_core:CreateUseableItem('diving_fill', function(source)
local success = lib.callback.await('qbx_divegear:client:fillTank', source) local tanks = getTankList(source)
if success then
exports.ox_inventory:RemoveItem(source, 'diving_fill', 1) if #tanks == 0 then
exports.qbx_core:Notify(source, locale('error.no_tank'), 'error')
return
end end
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) end)