Compare commits

...
11 Commits
Author SHA1 Message Date
D4rkst3randClaude Opus 5.5 056215d0ae fix: Befehlsausgabe in die F8-Konsole statt nur in die Serverkonsole (#81)
/tauchartikel und /tauchstand laufen jetzt ueber Diagnosebefehl: die Ausgabe geht an die F8-Konsole
des Aufrufers. Reine Konsolenbefehle sind fuer Admins im Spiel offen
(restricted group.admin, Betreiber 07.10.2026). print -> sag in den
betroffenen Dateien; ausserhalb eines Diagnosebefehls ist sag ein print.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 12:49:19 +02:00
D4rkst3r db7cef893d merge: feat/tauchkleidung-frauen 2026-09-29 00:33:55 +02:00
D4rkst3randClaude Opus 5.5 c2c5483c87 feat: Taucherflasche und Maske fuer Modell f
Gemessen 29.09.2026 (Betreiber): Flasche mp_f_christmas2017:1 (c:8), Maske
mp_f_christmas2017:0 (p:1) - wie m. d4rk_garment-Zeilen mit /tauchartikel
angelegt (39, 40).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 00:33:55 +02:00
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 921 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 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 = {
mask = 0,
tank = 0,
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,
--- gesteuert über den replizierten Statebag.
local lampOn = false
@@ -32,10 +37,6 @@ for i = 1, #config.warnAtSeconds do
if dangerThreshold == 0 or threshold < dangerThreshold then dangerThreshold = threshold 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
--- @return string mm:ss
@@ -127,50 +128,6 @@ local function disableScuba(outOfAir, ped)
SetPedMaxTimeUnderwater(ped, outOfAir and config.maxTimeUnderwaterOutOfAir or config.maxTimeUnderwater)
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 notifyIfUnavailable boolean? Rückmeldung, wenn das Outfit keine Lampe hat
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
-- besser als ein scheinbar kaputter Keybind.
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
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
--- und gibt den akzeptierten Stand zurück, damit Client und Inventar nicht auseinanderlaufen.
--- @param immediate boolean? true = Fire-and-Forget (Tod, Resource-Stop)
@@ -220,8 +169,10 @@ end
--- Gemeinsamer Teardown für Ablegen, leere Flasche und Tod.
--- @param outOfAir boolean?
--- @param immediateSync boolean?
local function removeGear(outOfAir, immediateSync)
syncTank(immediateSync)
--- @param ohneSync boolean? nach einem Patronentausch: der Client-Stand gehoert
--- 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
setLamp(false)
@@ -232,7 +183,6 @@ local function removeGear(outOfAir, immediateSync)
narcosisDamageBuffer = 0.0
engineDepthWarned = false
deleteGear()
disableScuba(outOfAir)
-- Stop breathing suit audio
end
@@ -272,7 +222,7 @@ end
lib.callback.register('d4rk_divegear:client:fillTank', function(tanks)
if IsPedSwimmingUnderWater(cache.ped) then
exports.qbx_core:Notify(locale('error.underwater'), 'error')
lib.notify({ description = locale('error.underwater'), type = 'error' })
return
end
@@ -281,11 +231,11 @@ lib.callback.register('d4rk_divegear:client:fillTank', function(tanks)
if not tank then return end
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
end
if not lib.progressBar({
if not exports.d4rk_lib:progress({
duration = config.refillTankTimeMs,
label = locale('info.filling_air'),
useWhileDead = false,
@@ -314,23 +264,6 @@ RegisterNetEvent('d4rk_divegear:client:tankRefilled', function(slot, capacity)
enableScuba()
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
--- verbliebenen Gasmenge, deshalb reicht der lineare Anteil an der Füllung.
--- @return number bar
@@ -394,6 +327,8 @@ end
exports('setHudVisible', setHudVisible)
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.
--- Läuft auch über Wasser weiter, weil getDecayFactor die Tiefe braucht - nur die
@@ -411,7 +346,9 @@ local function startHudThread()
lamp = locale('hud.lamp')
})
while currentGear.enabled do
local meinLauf = gearLauf
while currentGear.enabled and meinLauf == gearLauf do
currentDepth = currentDepth + (getDepth() - currentDepth) * 0.25
local visible = config.hudEnabled
@@ -432,6 +369,9 @@ local function startHudThread()
Wait(config.hudUpdateIntervalMs)
end
-- Abgeloest durch einen neuen Faden (Patronentausch): der zeigt weiter.
if meinLauf ~= gearLauf and currentGear.enabled then return end
currentDepth = 0.0
sendHudState(false)
end)
@@ -451,7 +391,7 @@ local function updateNarcosis(elapsed)
if currentDepth >= engineWarnAt then
if not engineDepthWarned then
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
else
engineDepthWarned = false
@@ -471,7 +411,7 @@ local function updateNarcosis(elapsed)
if activeTank and over >= -config.narcosis.warnBeforeMeters then
if not narcosisWarned then
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
else
narcosisWarned = false
@@ -502,8 +442,9 @@ local function startOxygenLevelDecrementerThread()
CreateThread(function()
local lastTick = GetGameTimer()
local sinceSync = 0.0
local meinLauf = gearLauf
while currentGear.enabled do
while currentGear.enabled and meinLauf == gearLauf do
Wait(250)
local now = GetGameTimer()
@@ -522,13 +463,13 @@ local function startOxygenLevelDecrementerThread()
if activeTank.oxygen <= threshold and not warned[threshold] then
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
if activeTank.oxygen <= 0 then
disableScuba(true)
exports.qbx_core:Notify(locale('error.out_of_air'), 'error')
lib.notify({ description = locale('error.out_of_air'), type = 'error' })
syncTank()
sinceSync = 0.0
-- Stop breathing suit audio
@@ -554,17 +495,14 @@ end
--- und Fahrzeugausstieg vom Spiel zurückgesetzt werden.
local function startGearWatchdogThread()
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
removeGear(false, true)
break
end
if not isGearIntact() then
deleteGear()
attachGear()
end
if activeTank and activeTank.oxygen > 0 then
enableScuba()
end
@@ -574,31 +512,103 @@ local function startGearWatchdogThread()
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
exports.qbx_core:Notify(locale('error.not_standing_up'), 'error')
lib.notify({ description = locale('error.not_standing_up'), type = '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')
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({
if not exports.d4rk_lib:progress({
duration = config.putOnSuitTimeMs,
label = locale('info.put_suit'),
label = locale('info.patrone_einsetzen'),
useWhileDead = false,
canCancel = true,
anim = {
@@ -607,26 +617,36 @@ local function putOnSuit()
blendIn = 8.0
}
}) then
activeTank = tank
warned = {}
deleteGear()
attachGear()
enableScuba()
currentGear.enabled = true
-- Initiate breathing suit audio
startOxygenLevelDecrementerThread()
startHudThread()
startGearWatchdogThread()
end
return
end
RegisterNetEvent('d4rk_divegear:client:useGear', function()
if currentGear.enabled then
takeOffSuit()
else
putOnSuit()
-- 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
exports('patroneBenutzen', function(_, slot)
einsetzen(type(slot) == 'table' and slot.slot or nil)
end)
lib.addKeybind({
@@ -659,60 +679,42 @@ lib.addKeybind({
lib.onCache('ped', function(ped)
if not currentGear.enabled then return end
deleteGear()
attachGear(ped)
if activeTank and activeTank.oxygen > 0 then
enableScuba(ped)
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
--- nur eine Slot-Nummer, die niemand mehr gegenprüfte, und man konnte unbegrenzt
--- weitertauchen. ox_inventory feuert bei jeder Änderung, also einmal nachfragen.
AddEventHandler('ox_inventory:updateInventory', function()
if not currentGear.enabled or not activeTank then return end
--[[
GTA NIMMT FLASCHE UND MASKE SELBST AB - gemessen am 29.09.2026 mit
/tauchprobe beobachten (Betreiber): im Moment, in dem "schwimmt" auf nein
springt (flaches Wasser, Boden unter den Fuessen), verschwanden Komponente 8
(global 123 = GTAs Taucherflasche) und Prop 1 gleichzeitig - bei
ausgeschaltetem Tauchgeraet, also nicht durch diesen Code. Die Items lagen
weiter in ihren Slots.
CPED_CONFIG_FLAG_DisableTakeOffScubaGear = 409 (NACHGESCHLAGEN in der
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
CreateThread(function()
local expected = activeTank
-- Der Thread startet erst im nächsten Tick, die Prüfung oben ist also
-- schon veraltet. Beim Ablegen passiert genau das: removeGear sichert die
-- Restluft, das ist selbst eine Inventar-Änderung, und bis hierher ist
-- 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)
-- 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
SetPedConfigFlag(PlayerPedId(), ABNEHMEN_AUS, true)
end)
lib.onCache('ped', function(ped)
SetPedConfigFlag(ped, ABNEHMEN_AUS, true)
end)
AddEventHandler('onResourceStop', function(resource)
if resource ~= GetCurrentResourceName() then return end
SetPedConfigFlag(cache.ped, ABNEHMEN_AUS, false)
syncTank(true)
setLamp(false)
applyNarcosisStage(0)
deleteGear()
SendNUIMessage({ action = 'state', visible = false })
SetEnableScuba(cache.ped, false)
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).
gearWatchdogIntervalMs = 1000,
--- Sekunden, die der Spieler ohne Ausrüstung unter Wasser überlebt (Vanilla-Verhalten).
maxTimeUnderwater = 50.0,
--- Sekunden Luft ohne Ausruestung - der NORMALWERT dieses Servers.
--- 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.
maxTimeUnderwaterOutOfAir = 1.0,
@@ -134,6 +138,8 @@ return {
--- Taste zum An- und Ausschalten. Der Spieler kann sie in den FiveM-
--- 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',
--- Die Lampe ist die spieleigene Scuba-Lampe (SetEnableScubaGearLight, seit
+39 -3
View File
@@ -26,9 +26,45 @@ local config = {
{ 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,
--- weil sowohl der Useable-Item-Handler als auch die Besitzprüfung sie brauchen.
gearItem = 'diving_gear',
--- SEIT 28.09.2026 SIND FLASCHE UND MASKE KLEIDUNG (plan-divegear.md).
--- Betreiber: "die Prop flasche zu einer richtigen taucherfalsche + maske
--- 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) gemessen am 29.09.2026 (Betreiber): dieselben Sammlungen und
--- Indizes wie m - Flasche mp_f_christmas2017:1, Maske mp_f_christmas2017:0.
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' },
{ pedset = 'f', komponente = 'c:8', collection = 'mp_f_christmas2017', drawable = 1, item = 'diving_tank' },
{ pedset = 'f', komponente = 'p:1', collection = 'mp_f_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',
--- Flaschen ohne Metadata als voll behandeln. Betrifft alles, was frisch aus
+13 -1
View File
@@ -13,11 +13,23 @@ shared_scripts {
'@qbx_core/modules/lib.lua',
}
server_script 'server/main.lua'
server_scripts {
'@d4rk_lib/server/diagnose_shared.lua',
'server/main.lua',
-- /tauchartikel: die Items in d4rk_item anlegen, nur auf Zuruf (28.09.2026).
'server/artikel.lua',
}
client_scripts {
-- lib.notify im Hausdesign (d4rk_lib) - VOR allen, die melden.
'@d4rk_lib/client/meldung_shared.lua',
'client/main.lua',
-- Die Konsole beim HUD anmelden (d4rk_hud, BauteilAnmelden).
'client/hud.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'
+45 -4
View File
@@ -10,11 +10,19 @@
"no_lamp_on_outfit": "Deine Tauchausrüstung hat keine Lampe.",
"crush_depth": "Lebensgefahr! Ab %d m zerdrückt dich der Wasserdruck.",
"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": {
"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": {
"put_suit": "Tauchanzug wird angezogen...",
@@ -22,7 +30,9 @@
"filling_air": "Sauerstoff wird aufgefüllt...",
"oxygen_low": "Nur noch %s Luft!",
"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": {
"choose_tank": "Tauchflasche wählen"
@@ -31,10 +41,41 @@
"air": "Luft",
"depth": "Tiefe",
"pressure": "Druck",
"lamp": "Lampe"
"lamp": "Lampe",
"konsole": "Tauchkonsole"
},
"keybind": {
"toggle_lamp": "Tauchlampe an/aus",
"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.",
"crush_depth": "Danger! Below %d m the water pressure will crush you.",
"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": {
"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": {
"put_suit": "Putting on your diving suit...",
@@ -22,7 +30,9 @@
"filling_air": "Filling air...",
"oxygen_low": "Only %s of air left!",
"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": {
"choose_tank": "Choose an air tank"
@@ -31,10 +41,41 @@
"air": "Air",
"depth": "Depth",
"pressure": "Pressure",
"lamp": "Lamp"
"lamp": "Lamp",
"konsole": "Dive console"
},
"keybind": {
"toggle_lamp": "Toggle dive lamp",
"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}`;
}
function render(state) {
hud.classList.toggle('hidden', !state.visible);
// Lage vom HUD (d4rk_hud, BauteilAnmelden - client/hud.lua). null = keine,
// 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 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 === 'lage') {
lage = data.lage || null;
lageAnwenden();
if (letzterStand) render(letzterStand);
}
});
// Damit sich das HUD im Browser ohne FiveM anschauen laesst.
+12
View File
@@ -60,6 +60,18 @@ body {
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 {
font-size: 9px;
font-weight: 600;
+225
View File
@@ -0,0 +1,225 @@
--[[
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.
SERVERKONSOLE ODER ADMIN IM SPIEL (Diagnosebefehl, restricted group.admin,
seit 07.10.2026 - vorher nur source 0; Ausgabe in die F8-Konsole, #81).
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
sag('^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
sag((' 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)
sag((' 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
Diagnosebefehl('tauchartikel', { restricted = 'group.admin' }, function(source, args)
local nachziehen = args[1] == 'nachziehen'
if GetResourceState(ITEMS) ~= 'started' then
sag('^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
sag('^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
sag('^1[d4rk_divegear] tauchartikel: adminItemList lieferte 0 Items - UNGUELTIG, nichts angelegt.^7')
return
end
sag(('[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
sag((' %-20s vorhanden, passt'):format(it.name))
elseif not nachziehen then
sag((' %-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)
sag((' %-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)
sag((' %-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)
--[[
/tauchstand <id> - WAS LIEGT IN DEN SLOTS? (29.09.2026; seit 07.10.2026 auch Admins im Spiel, F8)
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.
]]
Diagnosebefehl('tauchstand', { restricted = 'group.admin' }, function(source, args)
local ziel = tonumber(args[1])
if not ziel or not GetPlayerName(ziel) then
sag('[d4rk_divegear] tauchstand <server-id> - Spieler nicht gefunden.')
return
end
sag(('[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 {}
sag((' %-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)
+119 -27
View File
@@ -26,7 +26,7 @@ end
local function getTankSlot(source, slotId)
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
@@ -37,6 +37,19 @@ local function getTankSlot(source, slotId)
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.
@@ -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).
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
end
@@ -64,7 +77,7 @@ end
--- @param source number
--- @return table[]
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 = {}
for i = 1, #shared.tankNames do
@@ -79,7 +92,7 @@ local function getTankList(source)
tanks[#tanks + 1] = {
slot = slot.slot,
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),
capacity = tankConfig.capacity,
pressure = tankConfig.pressure,
@@ -99,9 +112,11 @@ local function syncTank(source, slotId, oxygen)
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 now = os.time()
@@ -118,28 +133,102 @@ 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)
--[[
WAS TRAEGT ER? - 28.09.2026, ersetzt verifyGear.
if slot and slot.name == name then
return { gear = (gearCount or 0) > 0, tank = slotId }
end
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)
-- 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)
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 {
gear = (gearCount or 0) > 0,
tank = found and found[1] and found[1].slot or nil
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)
@@ -154,15 +243,18 @@ AddEventHandler('playerDropped', function()
lastSync[source] = nil
end)
exports.qbx_core:CreateUseableItem(shared.gearItem, function(source)
TriggerClientEvent('d4rk_divegear:client:useGear', source)
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.qbx_core:Notify(source, locale('error.no_tank'), 'error')
exports.d4rk_bridge:Notify(source, locale('error.no_tank'), 'error')
return
end
@@ -175,12 +267,12 @@ exports.qbx_core:CreateUseableItem(shared.fillItem, function(source)
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
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.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)
end)