Files
d4rk_divegear/server/main.lua
T
D4rkst3randClaude Opus 5.5 e64a1bd3d0 feat: Flasche und Maske als Kleidung, Patronen tauschen, HUD-Anmeldung
- Tauchgeraet bereit = Taucherflasche (rigItem, Slot 31) mit Patrone UND
  Maske (maskItem, Slot 29) angezogen; Server getWorn statt verifyGear,
  geprueft bei jeder Inventaraenderung und nach jedem Umziehen.
  Props, diving_gear und der Anzieh-Balken entfallen.
- Die fuenf Flaschen sind Patronen: benutzen = in die getragene Flasche
  einsetzen, die alte kommt mit Restluft zurueck (patroneTauschen).
- Faeden mit Laufnummer, damit ein Tausch sie nicht verdoppelt.
- exports.ox_inventory -> exports.d4rk_inventory, Meldungen ueber
  lib.notify (meldung_shared) und d4rk_bridge:Notify.
- Konsole beim HUD angemeldet (client/hud.lua), 366 x 165 bei 1080p
  gemessen; ohne HUD bleibt die CSS-Position.
- Nachfuellen (diving_fill) stillgelegt: kommt spaeter ueber Shop/Crafting.

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

281 lines
9.5 KiB
Lua

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 in die getragene Flasche einsetzen (28.09.2026).
-- d4rk_inventory ruft den Rueckruf mit den Itemdaten samt Slot
-- (modules/bridge/qbx/server.lua: cb(source, data)).
for i = 1, #shared.tankNames do
exports.qbx_core:CreateUseableItem(shared.tankNames[i], function(source, data)
TriggerClientEvent('d4rk_divegear:client:patroneEinsetzen', source, data and data.slot)
end)
end
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)