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.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] tanks[#tanks + 1] = { slot = slot.slot, name = name, label = (exports.ox_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 local slot, tankConfig = getTankSlot(source, slotId) if not slot 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) --- Prüft, ob der Spieler Ausrüstung und Flasche überhaupt noch besitzt. --- Ohne das konnte man beides im Tauchgang weggeben und trotzdem weiteratmen - --- der Client hielt nur eine Slot-Nummer, die niemand mehr gegenprüfte. --- @return { gear: boolean, tank: number? } lib.callback.register('d4rk_divegear:server:verifyGear', function(source, slotId, name) local gearCount = exports.ox_inventory:Search(source, 'count', shared.gearItem) local slot = getTankSlot(source, slotId) if slot and slot.name == name then return { gear = (gearCount or 0) > 0, tank = slotId } end -- Nur umsortiert? Dann dieselbe Flaschensorte woanders im Inventar suchen, -- sonst kostet jedes Verschieben im Inventar die Ausrüstung. local found = shared.tankByName[name] and exports.ox_inventory:Search(source, 'slots', name) return { gear = (gearCount or 0) > 0, tank = found and found[1] and found[1].slot or nil } 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) exports.qbx_core:CreateUseableItem(shared.gearItem, function(source) TriggerClientEvent('d4rk_divegear:client:useGear', source) end) exports.qbx_core:CreateUseableItem(shared.fillItem, function(source) local tanks = getTankList(source) if #tanks == 0 then exports.qbx_core: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.ox_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.qbx_core:Notify(source, locale('success.tube_filled'), 'success') TriggerClientEvent('d4rk_divegear:client:tankRefilled', source, slot.slot, tankConfig.capacity) end)