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 = {} --- Restluft eines Slots. Ohne gesetzte Metadata (frisch aus dem Shop, per AddItem --- oder Admin-Befehl) entscheidet newTanksFull, ob die Flasche voll oder leer ist. --- Muss überall dieselbe Antwort geben, sonst hält die eine Stelle die Flasche für --- leer und die andere für voll. --- @return number local function readOxygen(slot, tankConfig) local stored = tonumber(slot.metadata and slot.metadata.oxygen) if not stored then return shared.newTanksFull and tankConfig.capacity or 0 end return math.max(0, math.min(stored, tankConfig.capacity)) end --- @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.d4rk_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 --- Die getragene Taucherflasche (Kleidung, shared.rigSlot) und die Patrone darin. --- @return table? slot nil = keine Flasche angezogen --- @return table? tankConfig nil = keine (gueltige) Patrone eingesetzt local function getRig(source) local slot = exports.d4rk_inventory:GetSlot(source, shared.rigSlot) if not slot or slot.name ~= shared.rigItem then return nil end local patrone = slot.metadata and slot.metadata.patrone return slot, patrone and shared.tankByName[patrone] or nil 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.d4rk_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.d4rk_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] tanks[#tanks + 1] = { slot = slot.slot, name = name, label = (exports.d4rk_inventory:Items(name) or {}).label or name, oxygen = readOxygen(slot, tankConfig), capacity = tankConfig.capacity, pressure = tankConfig.pressure, maxDepth = tankConfig.maxDepth, gas = tankConfig.gas } end end end return tanks end --- @return number? accepted local function syncTank(source, slotId, oxygen) oxygen = tonumber(oxygen) if not oxygen then return end -- Seit 28.09.2026 steckt die Luft in der GETRAGENEN Flasche (Kleidung); -- slotId bleibt im Aufruf, zaehlt aber nicht mehr. local slot, tankConfig = getRig(source) if not slot or not tankConfig then return end local current = readOxygen(slot, tankConfig) 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) --[[ WAS TRAEGT ER? - 28.09.2026, ersetzt verifyGear. Flasche (rigSlot) UND Maske (maskSlot) angezogen, und in der Flasche eine Patrone. Die Antwort nennt den Grund, wenn etwas fehlt - der Client fragt bei jeder Inventaraenderung, und "keine Maske" ist etwas anderes als "keine Patrone". ]] lib.callback.register('d4rk_divegear:server:getWorn', function(source) local rig, tankConfig = getRig(source) if not rig then return { ok = false, grund = 'keine_flasche' } end local maske = exports.d4rk_inventory:GetSlot(source, shared.maskSlot) if not maske or maske.name ~= shared.maskItem then return { ok = false, grund = 'keine_maske' } end if not tankConfig then return { ok = false, grund = 'keine_patrone' } end local patrone = rig.metadata.patrone return { ok = true, tank = { slot = rig.slot, name = patrone, label = (exports.d4rk_inventory:Items(patrone) or {}).label or patrone, oxygen = readOxygen(rig, tankConfig), capacity = tankConfig.capacity, pressure = tankConfig.pressure, maxDepth = tankConfig.maxDepth, gas = tankConfig.gas, }, } end) --[[ PATRONE TAUSCHEN - 28.09.2026. Die benutzte Patrone wandert in die getragene Flasche, die alte kommt mit ihrer Restluft als Item zurueck. REIHENFOLGE: erst pruefen, ob die alte Patrone ueberhaupt Platz hat, dann die neue nehmen, dann die alte geben - scheitert das Geben, bekommt der Spieler die neue zurueck. Zuletzt die Flasche beschreiben (Metadata KOPIEREN, SetMetadata ersetzt die ganze Tabelle - ENTSCHEIDUNGEN.md). ]] lib.callback.register('d4rk_divegear:server:patroneTauschen', function(source, cartSlot) cartSlot = math.tointeger(tonumber(cartSlot)) if not cartSlot or cartSlot == shared.rigSlot then return false, 'ungueltig' end local cart, cartConfig = getTankSlot(source, cartSlot) if not cart then return false, 'keine_patrone_im_slot' end local rig, altConfig = getRig(source) if not rig then return false, 'keine_flasche' end local altName = rig.metadata and rig.metadata.patrone local altLuft = altConfig and readOxygen(rig, altConfig) or nil if altConfig and not exports.d4rk_inventory:CanCarryItem(source, altName, 1) then return false, 'kein_platz' end local neuLuft = readOxygen(cart, cartConfig) if not exports.d4rk_inventory:RemoveItem(source, cart.name, 1, nil, cartSlot) then return false, 'fehler' end if altConfig then local meta = { oxygen = math.floor(altLuft), durability = math.floor(altLuft / altConfig.capacity * 100), } if not exports.d4rk_inventory:AddItem(source, altName, 1, meta) then exports.d4rk_inventory:AddItem(source, cart.name, 1, cart.metadata) return false, 'kein_platz' end end local meta = {} for key, value in pairs(rig.metadata or {}) do meta[key] = value end meta.patrone = cart.name meta.oxygen = math.floor(neuLuft) meta.durability = math.floor(neuLuft / cartConfig.capacity * 100) exports.d4rk_inventory:SetMetadata(source, rig.slot, meta) lastSync[source] = os.time() return true end) 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) -- Eine Patrone benutzen = sie einsetzen: ueber den Client-Export am Item -- (client/main.lua, patroneBenutzen). CreateUseableItem feuert fuer Items aus -- d4rk_items nie - consume ist dort immer gesetzt (28.09.2026). -- STILLGELEGT (Nachfuellen kommt spaeter ueber Shop/Crafting). Und ACHTUNG fuer -- dann: CreateUseableItem feuert fuer Items aus d4rk_items nie (consume ist -- dort immer gesetzt) - der Weg braucht einen Client-Export wie die Patronen. exports.qbx_core:CreateUseableItem(shared.fillItem, function(source) local tanks = getTankList(source) if #tanks == 0 then exports.d4rk_bridge:Notify(source, locale('error.no_tank'), 'error') return 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.d4rk_inventory:RemoveItem(source, shared.fillItem, 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.d4rk_bridge:Notify(source, locale('success.tube_filled'), 'success') TriggerClientEvent('d4rk_divegear:client:tankRefilled', source, slot.slot, tankConfig.capacity) end)