Compare commits

...
8 Commits
Author SHA1 Message Date
D4rkst3r c60174a3b3 merge: feat/projektstand 2026-09-29 00:29:15 +02:00
D4rkst3randClaude Opus 5.5 d4870444df fix: nach dem Patronentausch immer neu einlesen
Gleiche Groesse gegen gleiche Groesse galt als dieselbe Patrone - die Konsole
zeigte weiter die alte Restluft (Betreiber, 29.09.2026).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 00:28:05 +02:00
D4rkst3randClaude Opus 5.5 96c57d56a0 fix: Balken ueber d4rk_lib, /tauchstand zeigt die Slots
- lib.progressBar -> exports.d4rk_lib:progress (Betreiber: "beim benutzen
  kommt noch der alte balken von ox_lib").
- /tauchstand <id> (Serverkonsole): was liegt in rigSlot/maskSlot. Erster
  Lauf: beide Slots "clothing" mit der richtigen Kennung - vor dem
  Kleidungseintrag gekauft; getWorn antwortete also richtig.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 00:21:14 +02:00
D4rkst3randClaude Opus 5.5 bfe1d8ee2d fix: GTA nimmt Flasche und Maske nicht mehr selbst ab (Flag 409)
Gemessen mit /tauchprobe beobachten: beim Wechsel "schwimmt" -> nein
verschwanden Komponente 8 (global 123) und Prop 1 gleichzeitig, bei
ausgeschaltetem Tauchgeraet; die Items blieben in den Slots.
CPED_CONFIG_FLAG_DisableTakeOffScubaGear = 409 (nachgeschlagen) am
Spieler-Ped, nach jedem Ped-Wechsel neu, beim Stoppen ab.
/tauchprobe beobachten zeigt die Flagge mit an.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 00:16:48 +02:00
D4rkst3randClaude Opus 5.5 daefe46cb8 feat: Taucherflasche und Maske im Kleiderladen (Modell m)
Gemessen am 29.09.2026 (/d4rk_kleidungsstand, Betreiber):
Flasche m:c:8:mp_m_christmas2017:1 (Unterhemd, Slot 31),
Maske m:p:1:mp_m_christmas2017:0 - ein PROP im Brillenslot 27, nicht die
Maskenkomponente (angenommen war 29). texture -1: alle 26 Farben.
/tauchartikel legt die d4rk_garment-Zeilen auf Zuruf an. Modell f fehlt.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 00:10:11 +02:00
D4rkst3randClaude Opus 5.5 accee02a05 feat: Patronen per Client-Export, /tauchartikel legt die Items an
CreateUseableItem feuert fuer Items aus d4rk_items nie: consume ist dort
immer gesetzt (daten.lua:224), das Inventar erreicht den qbx-Rueckruf nur
bei consume == nil (server.lua:623). Die Patronen tragen deshalb
client_export = d4rk_divegear.patroneBenutzen und consume 0, wie
d4rk_medical. /tauchartikel legt die sieben Items ueber adminItemCreate an,
nur auf Zuruf; "nachziehen" stellt bei vorhandenen nur consume und Export
richtig. Flasche und Maske consume 0 wie das Kleidungsitem clothing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 23:52:27 +02:00
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
D4rkst3randClaude Opus 5.5 930ff6381f chore: Luft ohne Flasche 10 s (gemessen), /tauchprobe fuer die Kleidung
maxTimeUnderwater stand auf 50 mit "Vanilla-Verhalten"; auf d4rk-rp sind
es gemessen 10,00 s (/medwasserprobe). /tauchprobe liest, was
SetPedScubaGearVariation an der Kleidung aendert (Maske) und zeigt
Komponente 8 bei einem Drawable (Flasche) - plan-divegear.md Schritt 0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 23:40:24 +02:00
12 changed files with 919 additions and 213 deletions

No files matched your search

+50
View File
@@ -0,0 +1,50 @@
--[[
d4rk_divegear - die Tauchkonsole beim HUD anmelden (28.09.2026).
d4rk_hud verwaltet Lage, Groesse und Sichtbarkeit fremder Bauteile im
Editor (/hudbearbeiten) und speichert sie je Charakter; die Konsole
zeichnet sich weiter selbst (d4rk_hud/client/anmeldung.lua, Vertrag dort).
Laeuft d4rk_hud nicht, bleibt die Konsole, wo nui/style.css sie hinsetzt.
breite x hoehe: die Konsole bei 1080p, GEMESSEN am 28.09.2026 mit
d4rk-rp-planung/scripts/nui-pruefen/divegear-konsole.mjs (offsetWidth/
offsetHeight von #hud, --hud-scale 1.000): 366 x 165. Das Beispiel in
d4rk_hud/client/anmeldung.lua nennt 364 x 162 - das war nicht gemessen.
]]
local KEY = 'tauchkonsole'
local BREITE, HOEHE = 366, 165
local function anmelden()
if GetResourceState('d4rk_hud') ~= 'started' then return end
pcall(function()
exports.d4rk_hud:BauteilAnmelden(KEY, {
name = locale('hud.konsole'),
breite = BREITE,
hoehe = HOEHE,
vorgabe = { anker = 'unten-mitte', x = 0, y = 76, skala = 1.0, an = true },
})
end)
end
AddEventHandler('d4rk_hud:bauteilLage', function(key, lage)
if key ~= KEY then return end
SendNUIMessage({ action = 'lage', lage = lage })
end)
AddEventHandler('d4rk_hud:bauteileAnfragen', anmelden)
AddEventHandler('onClientResourceStart', function(resource)
if resource == 'd4rk_hud' then SetTimeout(1000, function() anmelden() end) end
end)
AddEventHandler('onClientResourceStop', function(resource)
-- Ohne HUD wieder die eigene Position.
if resource == 'd4rk_hud' then SendNUIMessage({ action = 'lage', lage = false }) end
end)
CreateThread(function()
Wait(1000)
anmelden()
end)
+171 -169
View File
@@ -1,12 +1,17 @@
local config = require 'config.client' local config = require 'config.client'
local shared = require 'config.shared' local shared = require 'config.shared'
--- Tauchgeraet bereit: Flasche mit Patrone UND Maske angezogen (28.09.2026 -
--- vorher hingen hier zwei Props, mask und tank).
local currentGear = { local currentGear = {
mask = 0,
tank = 0,
enabled = false enabled = false
} }
--- Laufnummer der Faeden (Verbrauch, HUD, Waechter). Wird beim Tauschen einer
--- Patrone sofort neu angelegt - ohne Nummer liefen die alten Faeden nach
--- ihrem Wait einfach weiter, und es gaebe sie doppelt.
local gearLauf = 0
--- Lampe an? Der eigentliche Lichtkegel wird in client/light.lua gezeichnet, --- Lampe an? Der eigentliche Lichtkegel wird in client/light.lua gezeichnet,
--- gesteuert über den replizierten Statebag. --- gesteuert über den replizierten Statebag.
local lampOn = false local lampOn = false
@@ -32,10 +37,6 @@ for i = 1, #config.warnAtSeconds do
if dangerThreshold == 0 or threshold < dangerThreshold then dangerThreshold = threshold end if dangerThreshold == 0 or threshold < dangerThreshold then dangerThreshold = threshold end
end end
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 --- @param seconds number
--- @return string mm:ss --- @return string mm:ss
@@ -127,50 +128,6 @@ local function disableScuba(outOfAir, ped)
SetPedMaxTimeUnderwater(ped, outOfAir and config.maxTimeUnderwaterOutOfAir or config.maxTimeUnderwater) SetPedMaxTimeUnderwater(ped, outOfAir and config.maxTimeUnderwaterOutOfAir or config.maxTimeUnderwater)
end end
local function deleteGear()
if currentGear.mask ~= 0 then
DetachEntity(currentGear.mask, false, true)
DeleteEntity(currentGear.mask)
currentGear.mask = 0
end
if currentGear.tank ~= 0 then
DetachEntity(currentGear.tank, false, true)
DeleteEntity(currentGear.tank)
currentGear.tank = 0
end
end
--- Hängt ein Prop hart an einen Ped-Bone.
--- useSoftPinning bleibt bewusst false: mit true darf die Physik-Engine die
--- Attachment bei Ragdoll und harten Anim-Übergängen lösen, dann fliegt das Prop weg.
local function attachProp(ped, model, bone, offset, rotation)
lib.requestModel(model)
local prop = CreateObject(model, 1.0, 1.0, 1.0, true, true, false)
SetEntityCollision(prop, false, false) --- Disable collision so it doesn't block bullets
AttachEntityToEntity(prop, ped, GetPedBoneIndex(ped, bone),
offset.x, offset.y, offset.z,
rotation.x, rotation.y, rotation.z,
false, -- p9
false, -- useSoftPinning ("if set to false attached entity will not detach when fixed")
false, -- collision
false, -- isPed
2, -- rotationOrder
true) -- syncRot
SetModelAsNoLongerNeeded(model)
return prop
end
--- @param ped number? default cache.ped
local function attachGear(ped)
ped = ped or cache.ped
currentGear.tank = attachProp(ped, TANK_MODEL, TANK_BONE, vec3(-0.25, -0.25, 0.0), vec3(180.0, 90.0, 0.0))
currentGear.mask = attachProp(ped, MASK_MODEL, MASK_BONE, vec3(0.0, 0.0, 0.0), vec3(180.0, 90.0, 0.0))
end
--- @param state boolean --- @param state boolean
--- @param notifyIfUnavailable boolean? Rückmeldung, wenn das Outfit keine Lampe hat --- @param notifyIfUnavailable boolean? Rückmeldung, wenn das Outfit keine Lampe hat
local function setLamp(state, notifyIfUnavailable) local function setLamp(state, notifyIfUnavailable)
@@ -187,18 +144,10 @@ local function setLamp(state, notifyIfUnavailable)
-- aber falls sie bei einem Ped-Model doch nicht anschlägt, ist eine Meldung -- aber falls sie bei einem Ped-Model doch nicht anschlägt, ist eine Meldung
-- besser als ein scheinbar kaputter Keybind. -- besser als ein scheinbar kaputter Keybind.
if state and notifyIfUnavailable and not IsScubaGearLightEnabled(cache.ped) then if state and notifyIfUnavailable and not IsScubaGearLightEnabled(cache.ped) then
exports.qbx_core:Notify(locale('error.no_lamp_on_outfit'), 'error') lib.notify({ description = locale('error.no_lamp_on_outfit'), type = 'error' })
end end
end end
local function isPropIntact(prop)
return prop ~= 0 and DoesEntityExist(prop) and GetEntityAttachedTo(prop) == cache.ped
end
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 --- 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. --- und gibt den akzeptierten Stand zurück, damit Client und Inventar nicht auseinanderlaufen.
--- @param immediate boolean? true = Fire-and-Forget (Tod, Resource-Stop) --- @param immediate boolean? true = Fire-and-Forget (Tod, Resource-Stop)
@@ -220,8 +169,10 @@ 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?
--- @param immediateSync boolean? --- @param immediateSync boolean?
local function removeGear(outOfAir, immediateSync) --- @param ohneSync boolean? nach einem Patronentausch: der Client-Stand gehoert
syncTank(immediateSync) --- zur ALTEN Patrone und darf die neue nicht ueberschreiben
local function removeGear(outOfAir, immediateSync, ohneSync)
if not ohneSync then syncTank(immediateSync) end
currentGear.enabled = false currentGear.enabled = false
setLamp(false) setLamp(false)
@@ -232,7 +183,6 @@ local function removeGear(outOfAir, immediateSync)
narcosisDamageBuffer = 0.0 narcosisDamageBuffer = 0.0
engineDepthWarned = false engineDepthWarned = false
deleteGear()
disableScuba(outOfAir) disableScuba(outOfAir)
-- Stop breathing suit audio -- Stop breathing suit audio
end end
@@ -272,7 +222,7 @@ end
lib.callback.register('d4rk_divegear:client:fillTank', function(tanks) lib.callback.register('d4rk_divegear:client:fillTank', function(tanks)
if IsPedSwimmingUnderWater(cache.ped) then if IsPedSwimmingUnderWater(cache.ped) then
exports.qbx_core:Notify(locale('error.underwater'), 'error') lib.notify({ description = locale('error.underwater'), type = 'error' })
return return
end end
@@ -281,11 +231,11 @@ lib.callback.register('d4rk_divegear:client:fillTank', function(tanks)
if not tank then return end if not tank then return end
if tank.oxygen >= tank.capacity then if tank.oxygen >= tank.capacity then
exports.qbx_core:Notify(locale('error.tank_already_full'), 'error') lib.notify({ description = locale('error.tank_already_full'), type = 'error' })
return return
end end
if not lib.progressBar({ if not exports.d4rk_lib:progress({
duration = config.refillTankTimeMs, duration = config.refillTankTimeMs,
label = locale('info.filling_air'), label = locale('info.filling_air'),
useWhileDead = false, useWhileDead = false,
@@ -314,23 +264,6 @@ RegisterNetEvent('d4rk_divegear:client:tankRefilled', function(slot, capacity)
enableScuba() enableScuba()
end) end)
local function takeOffSuit()
if lib.progressBar({
duration = config.takeOffSuitTimeMs,
label = locale('info.pullout_suit'),
useWhileDead = false,
canCancel = true,
anim = {
dict = 'clothingshirt',
clip = 'try_shirt_positive_d',
blendIn = 8.0
}
}) then
removeGear()
exports.qbx_core:Notify(locale('success.took_out'))
end
end
--- Manometer-Anzeige. Bei konstantem Volumen ist der Druck proportional zur --- Manometer-Anzeige. Bei konstantem Volumen ist der Druck proportional zur
--- verbliebenen Gasmenge, deshalb reicht der lineare Anteil an der Füllung. --- verbliebenen Gasmenge, deshalb reicht der lineare Anteil an der Füllung.
--- @return number bar --- @return number bar
@@ -394,6 +327,8 @@ end
exports('setHudVisible', setHudVisible) exports('setHudVisible', setHudVisible)
exports('isHudVisible', function() return not hudHidden end) exports('isHudVisible', function() return not hudHidden end)
-- Fuer /tauchprobe beobachten (client/probe.lua): ist das Tauchgeraet aktiv?
exports('geraetAktiv', function() return currentGear.enabled == true end)
--- Pflegt die geglättete Tiefe und schiebt den Zustand ins NUI. --- Pflegt die geglättete Tiefe und schiebt den Zustand ins NUI.
--- Läuft auch über Wasser weiter, weil getDecayFactor die Tiefe braucht - nur die --- Läuft auch über Wasser weiter, weil getDecayFactor die Tiefe braucht - nur die
@@ -411,7 +346,9 @@ local function startHudThread()
lamp = locale('hud.lamp') lamp = locale('hud.lamp')
}) })
while currentGear.enabled do local meinLauf = gearLauf
while currentGear.enabled and meinLauf == gearLauf do
currentDepth = currentDepth + (getDepth() - currentDepth) * 0.25 currentDepth = currentDepth + (getDepth() - currentDepth) * 0.25
local visible = config.hudEnabled local visible = config.hudEnabled
@@ -432,6 +369,9 @@ local function startHudThread()
Wait(config.hudUpdateIntervalMs) Wait(config.hudUpdateIntervalMs)
end end
-- Abgeloest durch einen neuen Faden (Patronentausch): der zeigt weiter.
if meinLauf ~= gearLauf and currentGear.enabled then return end
currentDepth = 0.0 currentDepth = 0.0
sendHudState(false) sendHudState(false)
end) end)
@@ -451,7 +391,7 @@ local function updateNarcosis(elapsed)
if currentDepth >= engineWarnAt then if currentDepth >= engineWarnAt then
if not engineDepthWarned then if not engineDepthWarned then
engineDepthWarned = true engineDepthWarned = true
exports.qbx_core:Notify(locale('error.crush_depth', math.floor(config.narcosis.engineDepthLimit)), 'error') lib.notify({ description = locale('error.crush_depth', math.floor(config.narcosis.engineDepthLimit)), type = 'error' })
end end
else else
engineDepthWarned = false engineDepthWarned = false
@@ -471,7 +411,7 @@ local function updateNarcosis(elapsed)
if activeTank and over >= -config.narcosis.warnBeforeMeters then if activeTank and over >= -config.narcosis.warnBeforeMeters then
if not narcosisWarned then if not narcosisWarned then
narcosisWarned = true narcosisWarned = true
exports.qbx_core:Notify(locale('info.depth_limit', math.floor(activeTank.maxDepth or 0)), 'warning') lib.notify({ description = locale('info.depth_limit', math.floor(activeTank.maxDepth or 0)), type = 'warning' })
end end
else else
narcosisWarned = false narcosisWarned = false
@@ -502,8 +442,9 @@ local function startOxygenLevelDecrementerThread()
CreateThread(function() CreateThread(function()
local lastTick = GetGameTimer() local lastTick = GetGameTimer()
local sinceSync = 0.0 local sinceSync = 0.0
local meinLauf = gearLauf
while currentGear.enabled do while currentGear.enabled and meinLauf == gearLauf do
Wait(250) Wait(250)
local now = GetGameTimer() local now = GetGameTimer()
@@ -522,13 +463,13 @@ local function startOxygenLevelDecrementerThread()
if activeTank.oxygen <= threshold and not warned[threshold] then if activeTank.oxygen <= threshold and not warned[threshold] then
warned[threshold] = true warned[threshold] = true
exports.qbx_core:Notify(locale('info.oxygen_low', formatTime(activeTank.oxygen)), 'warning') lib.notify({ description = locale('info.oxygen_low', formatTime(activeTank.oxygen)), type = 'warning' })
end end
end end
if activeTank.oxygen <= 0 then if activeTank.oxygen <= 0 then
disableScuba(true) disableScuba(true)
exports.qbx_core:Notify(locale('error.out_of_air'), 'error') lib.notify({ description = locale('error.out_of_air'), type = 'error' })
syncTank() syncTank()
sinceSync = 0.0 sinceSync = 0.0
-- Stop breathing suit audio -- Stop breathing suit audio
@@ -554,17 +495,14 @@ end
--- und Fahrzeugausstieg vom Spiel zurückgesetzt werden. --- und Fahrzeugausstieg vom Spiel zurückgesetzt werden.
local function startGearWatchdogThread() local function startGearWatchdogThread()
CreateThread(function() CreateThread(function()
while currentGear.enabled do local meinLauf = gearLauf
while currentGear.enabled and meinLauf == gearLauf do
if config.removeGearOnDeath and IsPedDeadOrDying(cache.ped, true) then if config.removeGearOnDeath and IsPedDeadOrDying(cache.ped, true) then
removeGear(false, true) removeGear(false, true)
break break
end end
if not isGearIntact() then
deleteGear()
attachGear()
end
if activeTank and activeTank.oxygen > 0 then if activeTank and activeTank.oxygen > 0 then
enableScuba() enableScuba()
end end
@@ -574,31 +512,103 @@ local function startGearWatchdogThread()
end) end)
end end
local function putOnSuit() --[[
ANLEGEN - 28.09.2026. Kein Balken und keine Props mehr: angezogen wird im
Inventar (Flasche und Maske sind Kleidung). Hier wird nur das Tauchgeraet
scharf geschaltet, wenn der Server sagt, dass beides getragen wird und
Luft in der Patrone ist.
]]
--- @param tank table aus d4rk_divegear:server:getWorn
--- @return boolean
local function anlegen(tank)
if not tank or (tank.oxygen or 0) <= 0 then return false end
activeTank = tank
warned = {}
gearLauf = gearLauf + 1
enableScuba()
currentGear.enabled = true
startOxygenLevelDecrementerThread()
startHudThread()
startGearWatchdogThread()
return true
end
--[[
NACHSEHEN, WAS ER TRAEGT - bei jeder Inventaraenderung und nach jedem
Umziehen. Mehrere Anstoesse in kurzer Folge fuehren zu EINER Frage mehr,
nicht zu vielen parallelen.
]]
local pruefenLaeuft, pruefenNochmal = false, false
local function pruefen()
if pruefenLaeuft then
pruefenNochmal = true
return
end
pruefenLaeuft = true
CreateThread(function()
repeat
pruefenNochmal = false
local erg = lib.callback.await('d4rk_divegear:server:getWorn', false)
if type(erg) == 'table' then
if erg.ok then
if not currentGear.enabled then
if anlegen(erg.tank) then
lib.notify({ description = locale('info.bereit', formatTime(erg.tank.oxygen)), type = 'info' })
end
elseif activeTank and activeTank.name ~= erg.tank.name then
-- Patrone getauscht: der Client-Stand gehoert zur alten.
removeGear(false, false, true)
anlegen(erg.tank)
end
elseif currentGear.enabled then
lib.notify({ description = locale('error.' .. (erg.grund or 'gear_gone')), type = 'error' })
removeGear(false)
end
end
until not pruefenNochmal
pruefenLaeuft = false
end)
end
AddEventHandler('d4rk:kleidung:angezogen', pruefen)
AddEventHandler('ox_inventory:updateInventory', pruefen)
CreateThread(function()
Wait(2000)
pruefen()
end)
--[[
EINE PATRONE BENUTZEN = SIE EINSETZEN. Nur an Land, mit Balken - wie
vorher das Anlegen.
UEBER EINEN CLIENT-EXPORT AM ITEM, NICHT CreateUseableItem (28.09.2026).
d4rk_items setzt consume immer (daten.lua:224, `or 1`), und d4rk_inventory
erreicht den qbx-Rueckruf nur bei consume == nil (server.lua:623) - fuer
Items aus unserer Tabelle feuerte er also nie. Die Patronen tragen
client_export = 'd4rk_divegear.patroneBenutzen' und consume = 0 (angelegt
mit /tauchartikel, server/artikel.lua); das Inventar ruft
exports.d4rk_divegear:patroneBenutzen(daten, slot) mit slot = { name,
slot, metadata } - dasselbe wie d4rk_medical `benutzen`.
]]
local function einsetzen(slot)
if IsPedSwimming(cache.ped) or cache.vehicle then if IsPedSwimming(cache.ped) or cache.vehicle then
exports.qbx_core:Notify(locale('error.not_standing_up'), 'error') lib.notify({ description = locale('error.not_standing_up'), type = 'error' })
return return
end end
local tanks = lib.callback.await('d4rk_divegear:server:getTanks', false) if not exports.d4rk_lib:progress({
if not tanks or #tanks == 0 then
exports.qbx_core:Notify(locale('error.need_otube'), 'error')
return
end
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
if lib.progressBar({
duration = config.putOnSuitTimeMs, duration = config.putOnSuitTimeMs,
label = locale('info.put_suit'), label = locale('info.patrone_einsetzen'),
useWhileDead = false, useWhileDead = false,
canCancel = true, canCancel = true,
anim = { anim = {
@@ -607,26 +617,36 @@ local function putOnSuit()
blendIn = 8.0 blendIn = 8.0
} }
}) then }) then
activeTank = tank return
warned = {}
deleteGear()
attachGear()
enableScuba()
currentGear.enabled = true
-- Initiate breathing suit audio
startOxygenLevelDecrementerThread()
startHudThread()
startGearWatchdogThread()
end end
-- Die laufende Luft zuerst sichern - danach gehoert der Client-Stand zur
-- alten Patrone und wird nicht mehr geschrieben (pruefen -> removeGear ohneSync).
if currentGear.enabled then syncTank() end
local ok, grund = lib.callback.await('d4rk_divegear:server:patroneTauschen', false, slot)
if not ok then
lib.notify({ description = locale('error.' .. tostring(grund or 'fehler')), type = 'error' })
return
end
lib.notify({ description = locale('success.patrone_getauscht'), type = 'success' })
--[[
IMMER NEU EINLESEN - 29.09.2026. Betreiber: "beim austauschen
restettet sich das hud aber nicht". pruefen() las die Patrone nur neu,
wenn sich ihr NAME aenderte - eine 5-Minuten-Patrone gegen eine andere
5-Minuten-Patrone galt als dieselbe, und die alte Restluft blieb stehen.
Die laufende Luft ist oben schon gesichert (syncTank vor dem Tausch),
also abbauen OHNE zu schreiben und frisch vom Server anlegen.
]]
if currentGear.enabled then removeGear(false, false, true) end
pruefen()
end end
RegisterNetEvent('d4rk_divegear:client:useGear', function() exports('patroneBenutzen', function(_, slot)
if currentGear.enabled then einsetzen(type(slot) == 'table' and slot.slot or nil)
takeOffSuit()
else
putOnSuit()
end
end) end)
lib.addKeybind({ lib.addKeybind({
@@ -659,60 +679,42 @@ lib.addKeybind({
lib.onCache('ped', function(ped) lib.onCache('ped', function(ped)
if not currentGear.enabled then return end if not currentGear.enabled then return end
deleteGear()
attachGear(ped)
if activeTank and activeTank.oxygen > 0 then if activeTank and activeTank.oxygen > 0 then
enableScuba(ped) enableScuba(ped)
end end
end) end)
--- Ausrüstung und Flasche können während des Tauchgangs das Inventar verlassen - --[[
--- weggegeben, abgelegt, vom Admin entfernt. Ohne diese Prüfung hielt der Client GTA NIMMT FLASCHE UND MASKE SELBST AB - gemessen am 29.09.2026 mit
--- nur eine Slot-Nummer, die niemand mehr gegenprüfte, und man konnte unbegrenzt /tauchprobe beobachten (Betreiber): im Moment, in dem "schwimmt" auf nein
--- weitertauchen. ox_inventory feuert bei jeder Änderung, also einmal nachfragen. springt (flaches Wasser, Boden unter den Fuessen), verschwanden Komponente 8
AddEventHandler('ox_inventory:updateInventory', function() (global 123 = GTAs Taucherflasche) und Prop 1 gleichzeitig - bei
if not currentGear.enabled or not activeTank then return end ausgeschaltetem Tauchgeraet, also nicht durch diesen Code. Die Items lagen
weiter in ihren Slots.
CreateThread(function() CPED_CONFIG_FLAG_DisableTakeOffScubaGear = 409 (NACHGESCHLAGEN in der
local expected = activeTank SetPedConfigFlag-Doku der Nativ-Datenbank) schaltet das ab. Bei uns zieht
nur das Inventar aus. Gesetzt am Spieler-Ped, nach jedem Ped-Wechsel neu
(onCache-Parameter, nicht cache.ped), beim Stoppen wieder ab - sonst
bliebe sie ohne diese Resource stehen.
]]
local ABNEHMEN_AUS = 409
-- Der Thread startet erst im nächsten Tick, die Prüfung oben ist also CreateThread(function()
-- schon veraltet. Beim Ablegen passiert genau das: removeGear sichert die SetPedConfigFlag(PlayerPedId(), ABNEHMEN_AUS, true)
-- Restluft, das ist selbst eine Inventar-Änderung, und bis hierher ist end)
-- activeTank längst nil.
if not expected or not currentGear.enabled then return end
local result = lib.callback.await('d4rk_divegear:server:verifyGear', false, expected.slot, expected.name) lib.onCache('ped', function(ped)
SetPedConfigFlag(ped, ABNEHMEN_AUS, true)
-- Während des Callbacks kann die Ausrüstung schon abgelegt worden sein.
if not currentGear.enabled or activeTank ~= expected then return end
if not result then return end
if not result.gear then
exports.qbx_core:Notify(locale('error.gear_gone'), 'error')
removeGear(true)
return
end
if not result.tank then
exports.qbx_core:Notify(locale('error.tank_gone'), 'error')
removeGear(true)
return
end
activeTank.slot = result.tank
end)
end) end)
AddEventHandler('onResourceStop', function(resource) AddEventHandler('onResourceStop', function(resource)
if resource ~= GetCurrentResourceName() then return end if resource ~= GetCurrentResourceName() then return end
SetPedConfigFlag(cache.ped, ABNEHMEN_AUS, false)
syncTank(true) syncTank(true)
setLamp(false) setLamp(false)
applyNarcosisStage(0) applyNarcosisStage(0)
deleteGear()
SendNUIMessage({ action = 'state', visible = false }) SendNUIMessage({ action = 'state', visible = false })
SetEnableScuba(cache.ped, false) SetEnableScuba(cache.ped, false)
SetPedMaxTimeUnderwater(cache.ped, config.maxTimeUnderwater) SetPedMaxTimeUnderwater(cache.ped, config.maxTimeUnderwater)
+160
View File
@@ -0,0 +1,160 @@
--[[
d4rk_divegear - /tauchprobe: welche Kleidung ist die Tauchmaske?
(plan-divegear.md Schritt 0, 28.09.2026)
Flasche und Maske werden Kleidungsstuecke aus dem Kleiderladen
(d4rk_garment). Dafuer braucht es Komponente, Sammlung und lokalen Index -
je Modell (m/f). Der Katalog hat keine Namen, also wird gemessen:
SetPedScubaGearVariation (0x36C6984C3ED0C911, PED, client) setzt laut
Nativ-Datenbank bei Freemode-Modellen die Tauchmaske. Diese Probe liest
ALLE Komponenten (0-11) und Props (0-7) vorher und nachher und nennt,
was sich geaendert hat - samt Sammlung, lokalem Index und Zahl der
Texturen.
GEGENPROBE: aendert sich gar nichts, steht "UNGUELTIG" da - das Native hat
dann nichts getan, und "keine Maske" waere die falsche Lesart.
DIE FLASCHE setzt das Native bei Freemode NICHT (laut Doku nur bei
player_*-Modellen). Nachgeschlagen, nicht gemessen: maennlich Komponente 8,
Drawable 123. Deshalb liest die Probe mit "flasche [n]" Komponente 8 bei
Drawable n (Vorgabe 123) - ob es die Flasche IST, sieht nur das Auge.
Zurueck zur eigenen Kleidung: /d4rk_anziehen (d4rk_inventory).
]]
local function modell(ped)
local m = GetEntityModel(ped)
if m == `mp_m_freemode_01` then return 'm' end
if m == `mp_f_freemode_01` then return 'f' end
return '?'
end
local function stand(ped)
local t = {}
for c = 0, 11 do
t['c' .. c] = {
art = 'c', id = c,
d = GetPedDrawableVariation(ped, c), tex = GetPedTextureVariation(ped, c),
sammlung = GetPedDrawableVariationCollectionName(ped, c) or 'base',
lokal = GetPedDrawableVariationCollectionLocalIndex(ped, c),
}
end
for p = 0, 7 do
t['p' .. p] = {
art = 'p', id = p,
d = GetPedPropIndex(ped, p), tex = GetPedPropTextureIndex(ped, p),
sammlung = GetPedPropCollectionName(ped, p) or 'base',
lokal = GetPedPropCollectionLocalIndex(ped, p),
}
end
return t
end
local function zeile(ped, e)
local texturen = e.art == 'c' and GetNumberOfPedTextureVariations(ped, e.id, e.d)
or (e.d >= 0 and GetNumberOfPedPropTextureVariations(ped, e.id, e.d) or 0)
return ('%s%-2d global %4d Textur %2d Sammlung %-14s lokal %4d Texturen %d'):format(
e.art, e.id, e.d, e.tex, tostring(e.sammlung ~= '' and e.sammlung or 'base'), e.lokal or -1, texturen or -1)
end
--[[
/tauchprobe beobachten [sekunden] - 29.09.2026. Betreiber: "bei der
taucherfalsche haben wir das problem er zieht sie aus wenn er an land ist".
Schreibt JEDE Aenderung an Flasche (Komponente 8) und Maske (Prop 1) mit
Zeit, Wasserzustand und ob das Tauchgeraet aktiv ist - damit man sieht,
WANN sie verschwindet (Wasser verlassen? Tauchgeraet aus? Neuanziehen?),
statt es zu raten. Alle 10 s eine Lebenszeile, damit Stille nicht wie
"keine Aenderung" aussieht, wenn die Probe gar nicht laeuft.
]]
local function beobachten(sekunden)
local ende = GetGameTimer() + sekunden * 1000
local start = GetGameTimer()
local letzt, lebenszeichen = nil, 0
print(('^5=== Tauchprobe beobachten, %d s ===^7'):format(sekunden))
print(' t Flasche (c8) Maske (p1) Wasser Schwimmt Unten Geraet Flag409')
while GetGameTimer() < ende do
local ped = PlayerPedId()
local c8 = ('%s:%d (g%d t%d)'):format(GetPedDrawableVariationCollectionName(ped, 8) or 'base',
GetPedDrawableVariationCollectionLocalIndex(ped, 8) or -1, GetPedDrawableVariation(ped, 8), GetPedTextureVariation(ped, 8))
local p1 = ('%s:%d (g%d t%d)'):format(GetPedPropCollectionName(ped, 1) or 'base',
GetPedPropCollectionLocalIndex(ped, 1) or -1, GetPedPropIndex(ped, 1), GetPedPropTextureIndex(ped, 1))
local function jn(x) return (x == true or x == 1) and 'j' or 'n' end
local okA, aktiv = pcall(function() return exports.d4rk_divegear:geraetAktiv() end)
local zeile = ('%-29s %-29s %s %s %s %s %s'):format(c8, p1, jn(IsEntityInWater(ped)),
jn(IsPedSwimming(ped)), jn(IsPedSwimmingUnderWater(ped)), okA and jn(aktiv) or '?',
jn(GetPedConfigFlag(ped, 409, true)))
local t = (GetGameTimer() - start) / 1000
if zeile ~= letzt then
print((' %5.1f %s'):format(t, zeile))
letzt = zeile
elseif t - lebenszeichen >= 10 then
print((' %5.1f (unveraendert)'):format(t))
lebenszeichen = t
end
Wait(250)
end
print('^5=== Tauchprobe beobachten: Ende ===^7')
end
RegisterCommand('tauchprobe', function(_, args)
local ped = PlayerPedId()
if args[1] == 'beobachten' then
CreateThread(function() beobachten(tonumber(args[2]) or 120) end)
lib.notify({ title = 'Tauchprobe', description = 'Beobachte - Ausgabe in F8.', type = 'info' })
return
end
print('')
print(('^5=== d4rk_divegear: Tauchprobe (Modell %s) ===^7'):format(modell(ped)))
if args[1] == 'flasche' then
local n = tonumber(args[2]) or 123
SetPedComponentVariation(ped, 8, n, 0, 0)
Wait(300)
local s = stand(ped).c8
print(' Komponente 8 gesetzt auf global ' .. n .. ':')
print(' ' .. zeile(ped, s))
print((' Ist das die Taucherflasche? Ansehen. Andere Nummer: /tauchprobe flasche <n>. '
.. 'Zurueck: /d4rk_anziehen'))
return
end
local vorher = stand(ped)
SetPedScubaGearVariation(ped)
Wait(500)
local nachher = stand(ped)
local anders = 0
for k, n in pairs(nachher) do
local v = vorher[k]
if v.d ~= n.d or v.tex ~= n.tex then
anders = anders + 1
print(' vorher ' .. zeile(ped, v))
print(' nachher ' .. zeile(ped, n))
end
end
if anders == 0 then
print('^1 UNGUELTIG: SetPedScubaGearVariation hat nichts geaendert - keine Aussage.^7')
else
print(('^2 %d Teil(e) geaendert.^7 Zurueck zur eigenen Kleidung: /d4rk_anziehen'):format(anders))
end
lib.notify({ title = 'Tauchprobe', description = 'Ergebnis in F8.', type = 'info' })
end, false)
+8 -2
View File
@@ -9,8 +9,12 @@ return {
--- Intervall des Watchdogs, der prüft ob die Ausrüstung noch am Ped hängt (ms). --- 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 Luft ohne Ausruestung - der NORMALWERT dieses Servers.
maxTimeUnderwater = 50.0, --- Stand bis 28.09.2026 auf 50.0 mit "Vanilla-Verhalten". GEMESSEN auf
--- d4rk-rp mit /medwasserprobe (d4rk_medical): 10,00 s an der Oberflaeche.
--- Mit 50 haette jeder nach dem ersten Ablegen fuenfmal so viel Luft - und
--- das Ertrinken (d4rk_medical, Config.Drowning) kaeme viel zu spaet.
maxTimeUnderwater = 10.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,
@@ -134,6 +138,8 @@ return {
--- Taste zum An- und Ausschalten. Der Spieler kann sie in den FiveM- --- Taste zum An- und Ausschalten. Der Spieler kann sie in den FiveM-
--- Tastenbelegungen unter "FiveM" umlegen. --- Tastenbelegungen unter "FiveM" umlegen.
--- L ist AUCH das Fahrzeugschloss (d4rk_vehicle, wirkt von aussen bis
--- 6 m) - beide feuern. Bewusst so gelassen, Betreiber 28.09.2026.
lampKey = 'l', lampKey = 'l',
--- Die Lampe ist die spieleigene Scuba-Lampe (SetEnableScubaGearLight, seit --- Die Lampe ist die spieleigene Scuba-Lampe (SetEnableScubaGearLight, seit
+37 -3
View File
@@ -26,9 +26,43 @@ local config = {
{ name = 'diving_tank_trimix', capacity = 900, pressure = 300, maxDepth = 100.0, gas = 'Trimix' }, -- 15 Minuten, tieftauglich { name = 'diving_tank_trimix', capacity = 900, pressure = 300, maxDepth = 100.0, gas = 'Trimix' }, -- 15 Minuten, tieftauglich
}, },
--- Item, das die Ausrüstung an- und auszieht, und das Füll-Item. Stehen hier, --- SEIT 28.09.2026 SIND FLASCHE UND MASKE KLEIDUNG (plan-divegear.md).
--- weil sowohl der Useable-Item-Handler als auch die Besitzprüfung sie brauchen. --- Betreiber: "die Prop flasche zu einer richtigen taucherfalsche + maske
gearItem = 'diving_gear', --- welche wir im kleider laden kaufen koennen", und "die flaschen groessen
--- soll man an der normalen taucherfalsche austauschen koennen".
---
--- rigItem die Taucherflasche aus dem Kleiderladen (d4rk_garment -> item).
--- Sie traegt die eingesetzte Patrone in der Metadata:
--- patrone = Name aus tanks (unten), oxygen = Restsekunden.
--- maskItem die Tauchmaske aus dem Kleiderladen.
--- rigSlot / maskSlot wo sie getragen werden - d4rk_inventory
--- shared/d4rk_slots.lua:71-96. GEMESSEN am 29.09.2026 mit
--- /d4rk_kleidungsstand (Betreiber, Modell m):
--- Flasche m:c:8:mp_m_christmas2017:1 -> 31 Unterhemd (Komponente 8)
--- Maske m:p:1:mp_m_christmas2017:0 -> 27 Brille (PROP 1, nicht
--- die Maskenkomponente 1 - angenommen war Slot 29)
---
--- Die fuenf `tanks` sind jetzt PATRONEN: Items im Inventar, die man an der
--- getragenen Flasche tauscht (Patrone benutzen). Die alten Props und das
--- Item diving_gear gibt es nicht mehr.
rigItem = 'diving_tank',
maskItem = 'diving_mask',
rigSlot = 31,
maskSlot = 27,
--- Die Kleidungsstuecke im Kleiderladen, die statt `clothing` das Item
--- ausgeben (d4rk_garment.item, angelegt mit /tauchartikel). drawable ist
--- der LOKALE Index in der Sammlung, texture -1 = alle Farben.
--- Weiblich (f): NOCH NICHT GEMESSEN - dort gibt es die Items erst, wenn
--- die Kennungen hier stehen.
kleidung = {
{ pedset = 'm', komponente = 'c:8', collection = 'mp_m_christmas2017', drawable = 1, item = 'diving_tank' },
{ pedset = 'm', komponente = 'p:1', collection = 'mp_m_christmas2017', drawable = 0, item = 'diving_mask' },
},
--- Das Fuell-Item. NICHT ANGELEGT: Nachfuellen kommt spaeter ueber einen
--- Shop oder Crafting (Betreiber, 28.09.2026). Der Weg bleibt stehen, damit
--- er sich dort anhaengen laesst - ohne das Item ist er nie erreichbar.
fillItem = 'diving_fill', fillItem = 'diving_fill',
--- Flaschen ohne Metadata als voll behandeln. Betrifft alles, was frisch aus --- Flaschen ohne Metadata als voll behandeln. Betrifft alles, was frisch aus
+12 -1
View File
@@ -13,11 +13,22 @@ shared_scripts {
'@qbx_core/modules/lib.lua', '@qbx_core/modules/lib.lua',
} }
server_script 'server/main.lua' server_scripts {
'server/main.lua',
-- /tauchartikel: die Items in d4rk_item anlegen, nur auf Zuruf (28.09.2026).
'server/artikel.lua',
}
client_scripts { client_scripts {
-- lib.notify im Hausdesign (d4rk_lib) - VOR allen, die melden.
'@d4rk_lib/client/meldung_shared.lua',
'client/main.lua', 'client/main.lua',
-- Die Konsole beim HUD anmelden (d4rk_hud, BauteilAnmelden).
'client/hud.lua',
'client/light.lua', 'client/light.lua',
-- Messung fuer die Umstellung auf Kleidung (plan-divegear.md Schritt 0).
-- Wird nach der Messung wieder entfernt.
'client/probe.lua',
} }
ui_page 'nui/index.html' ui_page 'nui/index.html'
+45 -4
View File
@@ -10,11 +10,19 @@
"no_lamp_on_outfit": "Deine Tauchausrüstung hat keine Lampe.", "no_lamp_on_outfit": "Deine Tauchausrüstung hat keine Lampe.",
"crush_depth": "Lebensgefahr! Ab %d m zerdrückt dich der Wasserdruck.", "crush_depth": "Lebensgefahr! Ab %d m zerdrückt dich der Wasserdruck.",
"gear_gone": "Deine Tauchausrüstung ist weg!", "gear_gone": "Deine Tauchausrüstung ist weg!",
"tank_gone": "Deine Tauchflasche ist weg!" "tank_gone": "Deine Tauchflasche ist weg!",
"keine_flasche": "Du trägst keine Taucherflasche.",
"keine_maske": "Ohne Tauchmaske kein Tauchgerät.",
"keine_patrone": "In deiner Taucherflasche steckt keine Patrone.",
"keine_patrone_im_slot": "Diese Patrone hast du nicht mehr.",
"kein_platz": "Kein Platz für die alte Patrone.",
"ungueltig": "Das geht nicht.",
"fehler": "Das hat nicht geklappt."
}, },
"success": { "success": {
"took_out": "Du hast deine Ausrüstung abgelegt!", "took_out": "Du hast deine Ausrüstung abgelegt!",
"tube_filled": "Du hast deine Tauchflasche erfolgreich aufgefüllt!" "tube_filled": "Du hast deine Tauchflasche erfolgreich aufgefüllt!",
"patrone_getauscht": "Patrone eingesetzt."
}, },
"info": { "info": {
"put_suit": "Tauchanzug wird angezogen...", "put_suit": "Tauchanzug wird angezogen...",
@@ -22,7 +30,9 @@
"filling_air": "Sauerstoff wird aufgefüllt...", "filling_air": "Sauerstoff wird aufgefüllt...",
"oxygen_low": "Nur noch %s Luft!", "oxygen_low": "Nur noch %s Luft!",
"tank_remaining": "%s verbleibend (%d%%) · %s bis %d m", "tank_remaining": "%s verbleibend (%d%%) · %s bis %d m",
"depth_limit": "Du näherst dich der Grenze deines Atemgases (%d m)!" "depth_limit": "Du näherst dich der Grenze deines Atemgases (%d m)!",
"bereit": "Tauchgerät bereit - %s Luft.",
"patrone_einsetzen": "Patrone wird eingesetzt..."
}, },
"menu": { "menu": {
"choose_tank": "Tauchflasche wählen" "choose_tank": "Tauchflasche wählen"
@@ -31,10 +41,41 @@
"air": "Luft", "air": "Luft",
"depth": "Tiefe", "depth": "Tiefe",
"pressure": "Druck", "pressure": "Druck",
"lamp": "Lampe" "lamp": "Lampe",
"konsole": "Tauchkonsole"
}, },
"keybind": { "keybind": {
"toggle_lamp": "Tauchlampe an/aus", "toggle_lamp": "Tauchlampe an/aus",
"toggle_hud": "Tauch-HUD ein-/ausblenden" "toggle_hud": "Tauch-HUD ein-/ausblenden"
},
"artikel": {
"diving_tank": {
"name": "Taucherflasche",
"text": "Tauchgerät zum Anziehen. Eine Patrone benutzen, um sie einzusetzen oder zu tauschen."
},
"diving_mask": {
"name": "Tauchmaske",
"text": "Ohne Maske kein Atmen unter Wasser."
},
"diving_tank_small": {
"name": "Flaschenpatrone (5 Min)",
"text": "Pressluft, 300 bar, bis 45 m. Reicht für etwa 5 Minuten."
},
"diving_tank_medium": {
"name": "Flaschenpatrone (10 Min)",
"text": "Pressluft, 300 bar, bis 45 m. Reicht für etwa 10 Minuten."
},
"diving_tank_large": {
"name": "Flaschenpatrone (15 Min)",
"text": "Pressluft, 300 bar, bis 45 m. Reicht für etwa 15 Minuten."
},
"diving_tank_xl": {
"name": "Flaschenpatrone (20 Min)",
"text": "Pressluft, 300 bar, bis 45 m. Reicht für etwa 20 Minuten."
},
"diving_tank_trimix": {
"name": "Trimix-Patrone (15 Min)",
"text": "Trimix, 300 bar, bis 100 m. Reicht für etwa 15 Minuten."
}
} }
} }
+45 -4
View File
@@ -10,11 +10,19 @@
"no_lamp_on_outfit": "Your diving suit doesn't have a lamp.", "no_lamp_on_outfit": "Your diving suit doesn't have a lamp.",
"crush_depth": "Danger! Below %d m the water pressure will crush you.", "crush_depth": "Danger! Below %d m the water pressure will crush you.",
"gear_gone": "Your diving gear is gone!", "gear_gone": "Your diving gear is gone!",
"tank_gone": "Your air tank is gone!" "tank_gone": "Your air tank is gone!",
"keine_flasche": "You are not wearing a scuba tank.",
"keine_maske": "No diving mask, no breathing apparatus.",
"keine_patrone": "There is no cartridge in your scuba tank.",
"keine_patrone_im_slot": "You no longer have that cartridge.",
"kein_platz": "No room for the old cartridge.",
"ungueltig": "That does not work.",
"fehler": "That did not work."
}, },
"success": { "success": {
"took_out": "You've taken your gear off!", "took_out": "You've taken your gear off!",
"tube_filled": "You've successfully refilled your air tank!" "tube_filled": "You've successfully refilled your air tank!",
"patrone_getauscht": "Cartridge inserted."
}, },
"info": { "info": {
"put_suit": "Putting on your diving suit...", "put_suit": "Putting on your diving suit...",
@@ -22,7 +30,9 @@
"filling_air": "Filling air...", "filling_air": "Filling air...",
"oxygen_low": "Only %s of air left!", "oxygen_low": "Only %s of air left!",
"tank_remaining": "%s remaining (%d%%) · %s down to %d m", "tank_remaining": "%s remaining (%d%%) · %s down to %d m",
"depth_limit": "You're approaching the limit of your breathing gas (%d m)!" "depth_limit": "You're approaching the limit of your breathing gas (%d m)!",
"bereit": "Breathing apparatus ready - %s of air.",
"patrone_einsetzen": "Inserting cartridge..."
}, },
"menu": { "menu": {
"choose_tank": "Choose an air tank" "choose_tank": "Choose an air tank"
@@ -31,10 +41,41 @@
"air": "Air", "air": "Air",
"depth": "Depth", "depth": "Depth",
"pressure": "Pressure", "pressure": "Pressure",
"lamp": "Lamp" "lamp": "Lamp",
"konsole": "Dive console"
}, },
"keybind": { "keybind": {
"toggle_lamp": "Toggle dive lamp", "toggle_lamp": "Toggle dive lamp",
"toggle_hud": "Toggle dive HUD" "toggle_hud": "Toggle dive HUD"
},
"artikel": {
"diving_tank": {
"name": "Scuba tank",
"text": "Breathing apparatus to wear. Use a cartridge to insert or swap it."
},
"diving_mask": {
"name": "Diving mask",
"text": "No mask, no breathing underwater."
},
"diving_tank_small": {
"name": "Tank cartridge (5 min)",
"text": "Compressed air, 300 bar, down to 45 m. Lasts about 5 minutes."
},
"diving_tank_medium": {
"name": "Tank cartridge (10 min)",
"text": "Compressed air, 300 bar, down to 45 m. Lasts about 10 minutes."
},
"diving_tank_large": {
"name": "Tank cartridge (15 min)",
"text": "Compressed air, 300 bar, down to 45 m. Lasts about 15 minutes."
},
"diving_tank_xl": {
"name": "Tank cartridge (20 min)",
"text": "Compressed air, 300 bar, down to 45 m. Lasts about 20 minutes."
},
"diving_tank_trimix": {
"name": "Trimix cartridge (15 min)",
"text": "Trimix, 300 bar, down to 100 m. Lasts about 15 minutes."
}
} }
} }
+34 -3
View File
@@ -118,10 +118,35 @@ function formatTime(seconds) {
return `${mm}:${ss}`; return `${mm}:${ss}`;
} }
function render(state) { // Lage vom HUD (d4rk_hud, BauteilAnmelden - client/hud.lua). null = keine,
hud.classList.toggle('hidden', !state.visible); // dann gilt die Position aus style.css. 28.09.2026.
let lage = null;
let letzterStand = null;
if (!state.visible) return; function lageAnwenden() {
if (!lage) {
hud.classList.remove('gelagert');
hud.style.left = '';
hud.style.top = '';
hud.style.removeProperty('--lage-scale');
return;
}
hud.classList.add('gelagert');
hud.style.left = `${lage.links}px`;
hud.style.top = `${lage.oben}px`;
hud.style.setProperty('--lage-scale', String(lage.skalierung ?? 1));
}
function render(state) {
letzterStand = state;
// Im HUD abgeschaltet, Fotomodus, Auswahl: das HUD sagt "nicht sichtbar".
const zeigen = state.visible && (!lage || lage.sichtbar !== false);
hud.classList.toggle('hidden', !zeigen);
if (!zeigen) return;
const capacity = state.capacity > 0 ? state.capacity : 1; const capacity = state.capacity > 0 ? state.capacity : 1;
const ratio = Math.min(1, Math.max(0, state.oxygen / capacity)); const ratio = Math.min(1, Math.max(0, state.oxygen / capacity));
@@ -171,6 +196,12 @@ window.addEventListener('message', (event) => {
} }
if (data.action === 'state') render(data); if (data.action === 'state') render(data);
if (data.action === 'lage') {
lage = data.lage || null;
lageAnwenden();
if (letzterStand) render(letzterStand);
}
}); });
// Damit sich das HUD im Browser ohne FiveM anschauen laesst. // Damit sich das HUD im Browser ohne FiveM anschauen laesst.
+12
View File
@@ -60,6 +60,18 @@ body {
pointer-events: none; pointer-events: none;
} }
/* Lage vom HUD (28.09.2026): linke obere Ecke in Bildschirmpixeln, Skalierung
fuer den bei 1080p gebauten Inhalt - rechnet d4rk_hud, hier nur anwenden. */
#hud.gelagert {
bottom: auto;
transform: scale(var(--lage-scale, 1));
transform-origin: 0 0;
}
#hud.gelagert.hidden {
transform: scale(var(--lage-scale, 1)) translateY(14px);
}
.caption { .caption {
font-size: 9px; font-size: 9px;
font-weight: 600; font-weight: 600;
+226
View File
@@ -0,0 +1,226 @@
--[[
d4rk_divegear - /tauchartikel: die Items in d4rk_item anlegen (28.09.2026).
DIE ITEMS GEHOEREN d4rk_items. Angelegt wird ueber dieselben Exporte wie
der Editor im Panel (adminItemList / adminItemCreate) - dasselbe Muster
wie d4rk_medical `medartikel` (server/artikel.lua dort).
NUR AUF ZURUF, NIE BEIM START: eine Saat bei jedem Start holte zurueck, was
der Betreiber geloescht hat (Gedaechtnis "Wiederholte Saat holt
Geloeschtes zurueck"). Vorhandene Items werden NICHT angefasst - was im
Panel gepflegt ist, gewinnt.
NUR AN DER SERVERKONSOLE (restricted, source 0).
Taucherflasche Kleidung (Kleiderladen, d4rk_garment -> item). Traegt
die Patrone in der Metadata. Gewicht GESETZT: Geraet
mit Atemregler ohne Patrone.
Tauchmaske Kleidung.
Patronen die fuenf aus config/shared.lua. Gewichte aus
items_for_ox_inventory.lua (dem Original).
client_export + consume = 0: siehe client/main.lua
(einsetzen) - CreateUseableItem feuert hier nie.
consume = 0 bei ALLEN - wie das Kleidungsitem `clothing` in
d4rk_inventory/data/items.lua. Mit dem Vorgabewert 1 (d4rk_items
daten.lua:224) koennte "Benutzen" im Inventar die Flasche verbrauchen.
Am 28.09.2026 zuerst ohne angelegt (Flasche, Maske: consume 1) - deshalb
gibt es `tauchartikel nachziehen`: stellt bei VORHANDENEN Items nur
consume und den Export richtig, alles andere bleibt, wie es im Panel steht.
stack = false bei allen: die Luft steht in der Metadata jedes Stuecks.
KEIN decay und keine durability-Schalter: metadata.durability zeigt den
Fuellstand und darf bei 0 nicht zum Loeschen fuehren (ENTSCHEIDUNGEN.md).
]]
local shared = require 'config.shared'
local ITEMS = 'd4rk_items'
local EXPORT = 'd4rk_divegear.patroneBenutzen'
-- Die Schalter einer Zeile als Boolean: adminItemUpdate liest sie mit
-- `w.x and 1 or 0`, eine 0 als Zahl waere in Lua wahr (wie d4rk_medical
-- server/artikel.lua, SCHALTER).
local SCHALTER = {
'stack', 'close_ui', 'nur_ausgabe', 'aktiv', 'cancel',
'disable_move', 'disable_car', 'disable_combat', 'allow_armed', 'durability',
}
local function wahr(x) return x == true or x == 1 end
local GEWICHT = {
[shared.rigItem] = 3000,
[shared.maskItem] = 400,
diving_tank_small = 4000,
diving_tank_medium = 7000,
diving_tank_large = 10000,
diving_tank_xl = 13000,
diving_tank_trimix = 11000,
}
local function liste()
local l = {
{ name = shared.rigItem },
{ name = shared.maskItem },
}
for i = 1, #shared.tankNames do
l[#l + 1] = { name = shared.tankNames[i], patrone = true }
end
return l
end
--[[
DIE KLEIDUNGSEINTRAEGE (d4rk_garment) - 29.09.2026. Ohne sie gibt der
Kleiderladen fuer Flasche und Maske `clothing` aus statt der Items
(d4rk_inventory d4rkStueckGeben -> d4rk_items GarmentInfo). Wie die Items:
nur auf Zuruf, vorhandene Zeilen werden nicht angefasst.
]]
local function kleidungAnlegen()
local ok, zeilen = pcall(function() return exports[ITEMS]:adminGarmentList() end)
if not ok or type(zeilen) ~= 'table' then
print('^1 Kleidung: adminGarmentList antwortet nicht - UNGUELTIG, nichts angelegt.^7')
return
end
for _, k in ipairs(shared.kleidung or {}) do
local vorhanden
for _, z in ipairs(zeilen) do
if z.pedset == k.pedset and (z.collection or '') == k.collection
and tonumber(z.drawable) == k.drawable and tonumber(z.texture) == -1 then
vorhanden = z
break
end
end
local name = ('%s %s %s:%d'):format(k.pedset, k.komponente, k.collection, k.drawable)
if vorhanden then
print((' Kleidung %-34s vorhanden (Item %s)%s'):format(name, tostring(vorhanden.item),
vorhanden.item == k.item and '' or ' ^3- soll ' .. k.item .. ', im Panel pruefen^7'))
else
local okC, erfolg, grund = pcall(function()
return exports[ITEMS]:adminGarmentCreate(0, {
pedset = k.pedset, komponente = k.komponente, collection = k.collection,
drawable = k.drawable, texture = -1,
label = locale('artikel.' .. k.item .. '.name'), item = k.item, aktiv = true,
})
end)
print((' Kleidung %-34s %s'):format(name,
(okC and erfolg == true) and '^2angelegt -> ' .. k.item .. '^7'
or ('^1FEHLER: %s^7'):format(okC and tostring(grund) or tostring(erfolg))))
end
end
end
RegisterCommand('tauchartikel', function(source, args)
if source ~= 0 then return end
local nachziehen = args[1] == 'nachziehen'
if GetResourceState(ITEMS) ~= 'started' then
print('^1[d4rk_divegear] tauchartikel: d4rk_items laeuft nicht - nichts angelegt.^7')
return
end
local ok, zeilen = pcall(function() return exports[ITEMS]:adminItemList() end)
if not ok or type(zeilen) ~= 'table' then
print('^1[d4rk_divegear] tauchartikel: adminItemList antwortet nicht - UNGUELTIG, nichts angelegt.^7')
return
end
local da = {}
for _, z in ipairs(zeilen) do if z.name then da[z.name] = z end end
-- GEGENPROBE: eine leere Liste hiesse "d4rk_items kennt kein einziges
-- Item" - das ist nie wahr, sondern eine kaputte Antwort.
if next(da) == nil then
print('^1[d4rk_divegear] tauchartikel: adminItemList lieferte 0 Items - UNGUELTIG, nichts angelegt.^7')
return
end
print(('[d4rk_divegear] tauchartikel: %d Items in d4rk_item bekannt.'):format(#zeilen))
for _, it in ipairs(liste()) do
local sollExport = it.patrone and EXPORT or nil
local z = da[it.name]
if z then
local istExport = (z.client_export and z.client_export ~= '') and z.client_export or nil
local passt = tonumber(z.consume) == 0 and istExport == sollExport
if passt then
print((' %-20s vorhanden, passt'):format(it.name))
elseif not nachziehen then
print((' %-20s ^3vorhanden, consume %s / Export %s - soll 0 / %s: tauchartikel nachziehen^7')
:format(it.name, tostring(z.consume), tostring(istExport), tostring(sollExport)))
else
local w = {}
for k, v in pairs(z) do w[k] = v end
for _, k in ipairs(SCHALTER) do w[k] = wahr(z[k]) end
w.consume = 0
w.client_export = sollExport or ''
local okU, erfolg, grund = pcall(function() return exports[ITEMS]:adminItemUpdate(0, z.id, w) end)
print((' %-20s %s'):format(it.name,
(okU and erfolg == true) and '^2nachgezogen (consume 0)^7'
or ('^1FEHLER: %s^7'):format(okU and tostring(grund) or tostring(erfolg))))
end
else
local werte = {
name = it.name,
label = locale('artikel.' .. it.name .. '.name'),
description = locale('artikel.' .. it.name .. '.text'),
weight = GEWICHT[it.name] or 1000,
stack = false,
close_ui = true,
aktiv = true,
consume = 0,
client_export = sollExport,
}
local okC, erfolg, grund = pcall(function() return exports[ITEMS]:adminItemCreate(0, werte) end)
print((' %-20s %s'):format(it.name,
(okC and erfolg == true) and '^2angelegt^7'
or ('^1FEHLER: %s^7'):format(okC and tostring(grund) or tostring(erfolg))))
end
end
kleidungAnlegen()
end, true)
--[[
/tauchstand <id> - WAS LIEGT IN DEN SLOTS? (29.09.2026, nur Serverkonsole)
Betreiber: "nachdem er durchgelaufen ist sagt er du traegst keine
taucherflasche". getWorn (server/main.lua) liest rigSlot und maskSlot -
dieser Befehl zeigt, was dort wirklich liegt: Itemname, Kleidungskennung,
Patrone. Ein `clothing` mit der richtigen Kennung heisst: vor dem
/tauchartikel-Eintrag gekauft, die Optik stimmt, das Item nicht.
]]
RegisterCommand('tauchstand', function(source, args)
if source ~= 0 then return end
local ziel = tonumber(args[1])
if not ziel or not GetPlayerName(ziel) then
print('[d4rk_divegear] tauchstand <server-id> - Spieler nicht gefunden.')
return
end
print(('[d4rk_divegear] tauchstand %d:'):format(ziel))
for _, s in ipairs({ { 'Flasche', shared.rigSlot, shared.rigItem }, { 'Maske', shared.maskSlot, shared.maskItem } }) do
local slot = exports.d4rk_inventory:GetSlot(ziel, s[2])
local m = slot and slot.metadata or {}
print((' %-8s Slot %d: %s (soll %s) garment %s patrone %s oxygen %s'):format(s[1], s[2],
slot and slot.name or '<leer>', s[3], tostring(m.garment), tostring(m.patrone), tostring(m.oxygen)))
end
end, true)
+119 -27
View File
@@ -26,7 +26,7 @@ end
local function getTankSlot(source, slotId) local function getTankSlot(source, slotId)
if type(slotId) ~= 'number' then return end if type(slotId) ~= 'number' then return end
local slot = exports.ox_inventory:GetSlot(source, slotId) local slot = exports.d4rk_inventory:GetSlot(source, slotId)
if not slot then return end if not slot then return end
@@ -37,6 +37,19 @@ local function getTankSlot(source, slotId)
return slot, tankConfig return slot, tankConfig
end 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. --- Schreibt die Restluft in die Metadata.
--- SetMetadata *ersetzt* die Metadata-Tabelle komplett (modules/inventory/server.lua), --- SetMetadata *ersetzt* die Metadata-Tabelle komplett (modules/inventory/server.lua),
--- deshalb wird der bestehende Inhalt kopiert statt nur oxygen gesetzt. --- deshalb wird der bestehende Inhalt kopiert statt nur oxygen gesetzt.
@@ -55,7 +68,7 @@ local function writeTank(source, slot, tankConfig, oxygen)
-- Items, sonst würde ox_inventory die leere Flasche löschen (items/server.lua). -- Items, sonst würde ox_inventory die leere Flasche löschen (items/server.lua).
metadata.durability = math.floor(oxygen / tankConfig.capacity * 100) metadata.durability = math.floor(oxygen / tankConfig.capacity * 100)
exports.ox_inventory:SetMetadata(source, slot.slot, metadata) exports.d4rk_inventory:SetMetadata(source, slot.slot, metadata)
return oxygen return oxygen
end end
@@ -64,7 +77,7 @@ end
--- @param source number --- @param source number
--- @return table[] --- @return table[]
local function getTankList(source) local function getTankList(source)
local found = exports.ox_inventory:Search(source, 'slots', shared.tankNames) local found = exports.d4rk_inventory:Search(source, 'slots', shared.tankNames)
local tanks = {} local tanks = {}
for i = 1, #shared.tankNames do for i = 1, #shared.tankNames do
@@ -79,7 +92,7 @@ local function getTankList(source)
tanks[#tanks + 1] = { tanks[#tanks + 1] = {
slot = slot.slot, slot = slot.slot,
name = name, name = name,
label = (exports.ox_inventory:Items(name) or {}).label or name, label = (exports.d4rk_inventory:Items(name) or {}).label or name,
oxygen = readOxygen(slot, tankConfig), oxygen = readOxygen(slot, tankConfig),
capacity = tankConfig.capacity, capacity = tankConfig.capacity,
pressure = tankConfig.pressure, pressure = tankConfig.pressure,
@@ -99,9 +112,11 @@ local function syncTank(source, slotId, oxygen)
if not oxygen then return end if not oxygen then return end
local slot, tankConfig = getTankSlot(source, slotId) -- 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 then return end if not slot or not tankConfig then return end
local current = readOxygen(slot, tankConfig) local current = readOxygen(slot, tankConfig)
local now = os.time() local now = os.time()
@@ -118,28 +133,102 @@ end
lib.callback.register('d4rk_divegear:server:getTanks', getTankList) 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 - WAS TRAEGT ER? - 28.09.2026, ersetzt verifyGear.
--- 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 Flasche (rigSlot) UND Maske (maskSlot) angezogen, und in der Flasche eine
return { gear = (gearCount or 0) > 0, tank = slotId } Patrone. Die Antwort nennt den Grund, wenn etwas fehlt - der Client fragt
end 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)
-- Nur umsortiert? Dann dieselbe Flaschensorte woanders im Inventar suchen, if not rig then return { ok = false, grund = 'keine_flasche' } end
-- sonst kostet jedes Verschieben im Inventar die Ausrüstung.
local found = shared.tankByName[name] and exports.ox_inventory:Search(source, 'slots', name) 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 { return {
gear = (gearCount or 0) > 0, ok = true,
tank = found and found[1] and found[1].slot or nil 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) 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) lib.callback.register('d4rk_divegear:server:syncTank', function(source, slotId, oxygen)
return syncTank(source, slotId, oxygen) return syncTank(source, slotId, oxygen)
end) end)
@@ -154,15 +243,18 @@ AddEventHandler('playerDropped', function()
lastSync[source] = nil lastSync[source] = nil
end) end)
exports.qbx_core:CreateUseableItem(shared.gearItem, function(source) -- Eine Patrone benutzen = sie einsetzen: ueber den Client-Export am Item
TriggerClientEvent('d4rk_divegear:client:useGear', source) -- (client/main.lua, patroneBenutzen). CreateUseableItem feuert fuer Items aus
end) -- 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) exports.qbx_core:CreateUseableItem(shared.fillItem, function(source)
local tanks = getTankList(source) local tanks = getTankList(source)
if #tanks == 0 then if #tanks == 0 then
exports.qbx_core:Notify(source, locale('error.no_tank'), 'error') exports.d4rk_bridge:Notify(source, locale('error.no_tank'), 'error')
return return
end end
@@ -175,12 +267,12 @@ exports.qbx_core:CreateUseableItem(shared.fillItem, function(source)
if not slot then return end if not slot then return end
-- Nach dem Progressbar nochmal prüfen: das Item kann in der Zwischenzeit weg sein. -- 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 if not exports.d4rk_inventory:RemoveItem(source, shared.fillItem, 1) then return end
writeTank(source, slot, tankConfig, tankConfig.capacity) writeTank(source, slot, tankConfig, tankConfig.capacity)
lastSync[source] = os.time() lastSync[source] = os.time()
-- Erst melden wenn das Item tatsächlich weg und die Flasche voll ist. -- Erst melden wenn das Item tatsächlich weg und die Flasche voll ist.
exports.qbx_core:Notify(source, locale('success.tube_filled'), 'success') exports.d4rk_bridge:Notify(source, locale('success.tube_filled'), 'success')
TriggerClientEvent('d4rk_divegear:client:tankRefilled', source, slot.slot, tankConfig.capacity) TriggerClientEvent('d4rk_divegear:client:tankRefilled', source, slot.slot, tankConfig.capacity)
end) end)