# d4rk_divegear — Umbauplan Fork von [Qbox-project/qbx_divegear](https://github.com/Qbox-project/qbx_divegear) (v1.0.1). Die Upstream-Historie ist vollständig erhalten und liegt auf dem Remote `upstream`, damit spätere Upstream-Änderungen mergebar bleiben. **Ziel:** Gesyncte Taschenlampe, verschiedene Flaschengrößen mit echter Laufzeit, Tiefenanzeige, und Ausrüstung die bei Animationen nicht mehr verschwindet. --- ## Ausgangslage (Analyse des Originals) `client/main.lua` hat 173 Zeilen, `server/main.lua` 10. Drei strukturelle Probleme, die die neuen Features blockieren: 1. **Sauerstoff ist eine lokale Client-Variable** (`local oxygenLevel = 0`) — nicht persistent, nicht am Item hängend, weg bei Relog/Restart. Blockiert Flaschengrößen. 2. **Laufzeit-Rechenfehler:** `startingOxygenLevel = 100` bei `decayRate = 1` pro Sekunde ergibt **100 Sekunden**, nicht Minuten. Die Vergleiche `oxygenLevel % 10 == 0` und `oxygenLevel == 0` (client/main.lua:121, :124) brechen, sobald `decayRate` fraktional wird. 3. **Attachment ist soft-pinned** (client/main.lua:70, :75) — die Physik-Engine darf die Props bei Ragdoll/Anims lösen. Kein Ped-Swap-Handling, kein Watchdog. --- ## Phasen Reihenfolge ist bewusst gewählt: Phase 1 und 2 sind Fundament, Phase 3–5 bauen darauf auf. ### Phase 1 — Gear-Persistenz (Bugfix, ~1 h) Zuerst, weil kleinster Aufwand, echter Bug, und die Taschenlampe dieselbe Attach-Logik nutzt. - [x] `attachGear()` auf harte Attachment umstellen: `useSoftPinning = false`, `p9 = false`, `syncRot = true` — ausgelagert in einen `attachProp()`-Helper, den Phase 5 für die Lampe mitbenutzt. Laut Native-Doku heißt `useSoftPinning` false: *"attached entity will not detach when fixed"* — genau das wollen wir - [x] `isGearIntact()` Helper: `DoesEntityExist` + `GetEntityAttachedTo(...) == cache.ped` für beide Props - [x] Watchdog-Thread (`gearWatchdogIntervalMs`, default 1000 ms) solange `currentGear.enabled`: bei Defekt `deleteGear()` + `attachGear()`, und `enableScuba()` neu setzen (`SetEnableScuba`/`SetPedMaxTimeUnderwater` werden bei Respawn/Fahrzeugausstieg vom Spiel zurückgesetzt) - [x] `lib.onCache('ped', ...)` → bei Model-Wechsel (Kleiderladen, Skin-Change) Props neu anhängen, Scuba-State neu setzen - [x] Death-Handler: `IsPedDeadOrDying`-Check im Watchdog statt eigenem Event — keine Abhängigkeit zu qbx_medical o.ä. Sichern der Restluft kommt in Phase 2 dazu - [x] Gemeinsamer `removeGear(outOfAir)`-Teardown für Ablegen, leere Flasche und Tod - [x] `onResourceStop` Cleanup-Handler für Props **Zu `SetPedMaxTimeUnderwater`:** war doch kein Bug. Die `50.0` beim Ablegen und die `1.0` bei leerer Flasche sind zwei verschiedene Absichten (normal weiterschwimmen vs. ohne Luft absaufen). Beide Werte sind jetzt als `maxTimeUnderwater` / `maxTimeUnderwaterOutOfAir` in der Config statt als Magic Number im Code. ### Phase 2 — Sauerstoff-Logik neu (~1 h) - [x] Umstellung von "Punkte" auf **Restsekunden** (`oxygenSeconds`) - [x] Decrementer-Thread auf `GetGameTimer()`-Delta statt gezählter `Wait(1000)` (driftfrei, überlebt Frame-Drops) - [x] Float-Vergleiche raus, Warnschwellen explizit als Config-Liste (z.B. `warnAtSeconds = { 60, 30, 10 }`), jede Schwelle einmal auslösen - [x] Anzeige als `mm:ss` statt roher Zahl - [x] Persistenz: Restluft alle ~15 s und beim Ablegen per Server-Event in die ox_inventory-Metadata des Tank-Items zurückschreiben (`exports.ox_inventory:SetMetadata`) - [x] Beim Anlegen Metadata lesen → Restluft übernehmen - [ ] **Offen:** Beim Spawn/Resource-Start die angelegte Ausrüstung automatisch wieder anziehen. Die Restluft überlebt bereits (liegt auf der Flasche), aber nach einem Relog steht man ohne Maske da und muss sie neu anlegen **Serverseitig validieren:** Der Client schickt den O2-Wert. Server prüft Plausibilität (nur Abnahme, nicht mehr als seit letztem Update vergehen konnte), sonst ist das ein trivialer Cheat-Vektor. ### Phase 3 — Flaschengrößen (~1 h, nach Phase 2 trivial) Vier Items mit unterschiedlicher `capacity` in Sekunden: | Item | Laufzeit | capacity | |---|---|---| | `diving_tank_small` | 5 min | 300 | | `diving_tank_medium` | 10 min | 600 | | `diving_tank_large` | 15 min | 900 | | `diving_tank_xl` | 20 min | 1200 | - [x] `capacity` in `config/shared.lua` (Client *und* Server brauchen sie), Label und Gewicht im Item-Snippet. Kein Prop-Scale — siehe Optik-Hinweis unten - [x] **`items_for_ox_inventory.lua`** als Snippet-Datei zum manuellen Reinkopieren in `ox_inventory/data/items.lua` — kein `RegisterItem`-Export - [x] `metadata.oxygen` (Restsekunden) + `metadata.durability` (Prozent, damit der Füllstand direkt im Inventar-Slot sichtbar ist) - [x] `diving_fill` füllt bis `capacity` des jeweiligen Tanks statt auf Fixwert - [x] Verbrauch skaliert mit Tiefe: `decayRate * (1 + depth / 30)` — physikalisch korrekt und macht große Flaschen erst sinnvoll (per Config abschaltbar) **Optik:** `p_s_scuba_tank_s` ist das einzige Basegame-Tank-Prop. Größenunterschied über `SetEntityScale` ist bei attached Objects unzuverlässig — entweder akzeptieren, dass alle vier gleich aussehen, oder Custom-Model. Für v1: gleich aussehen. ### Phase 4 — NUI-HUD: Tiefe + Sauerstoff (~2,5 h) Tiefenanzeige und O2-Anzeige zusammen als ein NUI-Overlay — `qbx.drawText2d` (aktuell `oxygenLevel..'⏱'` in der Bildschirmecke) fliegt raus. **Datenseite (~30 min):** - [x] Tiefe = Wasseroberfläche − Ped-Z, via `GetWaterHeight` (Fallback `GetWaterHeightNoWaves`) - [x] Wert geglättet (Lerp) damit die Zahl nicht zappelt - [x] Liefert gleichzeitig den Faktor für den tiefenabhängigen Verbrauch aus Phase 3 **NUI (~2 h):** - [x] O2 als Balken/Ring + `mm:ss` Restzeit, Farbwechsel an den Warnschwellen aus Phase 2, Pulsieren im kritischen Bereich - [x] Tiefe in Metern, Warnfarbe ab Config-Tiefe - [x] Slot für den Lampen-Status (wird in Phase 5 befüllt) - [x] Sichtbar nur wenn Gear angelegt **und** unter Wasser, mit Fade-In/Out - [x] Updates gedrosselt per `SendNUIMessage` (~4/s statt jeden Frame) - [x] Kein `backdrop-filter` (bekannter FiveM-Bug → schwarzer Hintergrund) - [x] `hudEnabled`, `hudUpdateIntervalMs` und `deepWarningDepth` in `config/client.lua` - [x] **Nachgereicht:** Manometer in bar. `pressure * (Restluft / capacity)` — bei konstantem Volumen ist der Druck proportional zur verbliebenen Gasmenge, die lineare Abbildung ist also korrekt. Alle Flaschen starten bei 300 bar; dass die große Flasche denselben Druck zeigt wie die kleine, ist gewollt (in echt macht das Volumen die Laufzeit). Unter `reservePressure` wird rot eingefärbt - [x] **Nachgereicht:** HUD ausblendbar für Unterwasser-Screenshots. Drei Wege, weil `qbx_core` selbst keinen HUD-Toggle hat (es feuert nur `hud:client:*` an eine separate HUD-Resource): `IsHudHidden()` folgen, eigene Taste, und der Export `setHudVisible(bool)` - [ ] **Offen:** Position und Größe des HUD sind bisher nur in `nui/style.css` einstellbar, nicht in der Config ### Phase 5 — Gesyncte Taschenlampe (~2–3 h) **Nachträglich komplett ersetzt.** Der ganze Abschnitt unten beschreibt den DrawSpotLight-Eigenbau — der ist raus. GTA hat seit v1493 eine eigene Lampe an der Tauchausrüstung (`SetEnableScubaGearLight`, `0xEE2476B9EE4A094F`), gefunden über [wobozkyng/esx_scuba](https://github.com/wobozkyng/esx_scuba). Die sitzt richtig, leuchtet in Blickrichtung und macht Prop, Offsets, Kegelwerte, Distanzfilter und Draw-Thread überflüssig. Geblieben ist nur der Statebag, weil der Flag ein lokaler Ped-Zustand ist. Einschränkung: die Lampe hängt an der Scuba-Kleidung. Der ursprüngliche Plan zum Vergleich: - [x] Torch-Prop mit derselben harten Attach-Methode aus Phase 1 anhängen (Kopf-Bone oder Hand, Config) - [x] Toggle per `RegisterKeyMapping`/`lib.addKeybind` - [x] State replizieren: `LocalPlayer.state:set('divelight', true, true)` — Statebag statt Event-Spam - [x] Draw-Thread: über `GetActivePlayers()`, `Player(serverId).state.divelight` prüfen, `DrawSpotLightWithShadow` zeichnen - [x] **Richtung anders gelöst als geplant:** statt Prop-Matrix und Objekt-Pool werden zwei *bone-relative* Punkte am Kopf-Bone benutzt. Die Offsets von `GetPedBoneCoords` sind laut Native-Doku "relative to the bone's rotation", die Differenz zweier solcher Punkte ergibt also die Blickrichtung inklusive Pitch. Das löst das Problem (`GetEntityForwardVector` würde beim waagerecht im Wasser liegenden Ped stur horizontal leuchten), ohne fremde Props über `GetGamePool('CObject')` suchen und cachen zu müssen — der Renderer braucht dadurch überhaupt keinen Prop-Zugriff und kommt mit dem Statebag allein aus - [x] Prop-Model wird über `IsModelValid`/`IsModelInCdimage` geprüft, weil `lib.requestModel` bei ungültigen Models einen Error wirft. Ein falscher Prop-Name kostet so nur die Optik, nicht die ganze Ausrüstung - [x] Performance: Distanzfilter ~40–50 m, max. 3–4 gleichzeitige Lichter (`DrawSpotLightWithShadow` ist teuer) - [x] Lampen-Status (An/Aus) in das HUD aus Phase 4 einhängen — keine Batterie, die Lampe läuft solange Sauerstoff da ist --- ## Zielstruktur Die Config bleibt beim Upstream-Pattern (`config/client.lua` + `require`) statt beim `shared_scripts`-Template, damit spätere Upstream-Änderungen mergebar bleiben. Tatsächlich gebaut (die geplante Aufteilung in `oxygen.lua`/`hud.lua` ist entfallen — die teilen sich alle denselben Zustand mit `main.lua`, eine Trennung hätte nur Plumbing erzeugt. `light.lua` hat dagegen eine echte Naht: es kommt mit dem Statebag allein aus und kennt weder Ausrüstung noch Flasche): ``` d4rk_divegear/ ├── fxmanifest.lua ├── config/ │ ├── client.lua # Timings, Tiefe, Licht, HUD │ └── shared.lua # Tank-Definitionen (Client + Server brauchen capacity) ├── client/ │ ├── main.lua # Gear an/aus, Watchdog, Sauerstoff, HUD-Bridge │ └── light.lua # Statebag + Draw-Thread, ohne gemeinsamen Zustand ├── nui/ │ ├── index.html │ ├── style.css │ └── script.js ├── server/ │ └── main.lua # Useable Items, Metadata schreiben + validieren ├── locales/ │ ├── de.json │ └── en.json └── items_for_ox_inventory.lua # Snippet zum Reinkopieren ``` Locales: DE + EN werden gepflegt, die 12 übrigen Upstream-Sprachen bleiben liegen (neue Keys fehlen dort → ox_lib fällt auf den Key-Namen zurück, kein Crash). --- ## Aufwand | Phase | Aufwand | |---|---| | 1 — Gear-Persistenz | ~1 h | | 2 — Sauerstoff-Logik | ~1 h | | 3 — Flaschengrößen | ~1 h | | 4 — NUI-HUD (Tiefe + O2) | ~2,5 h | | 5 — Taschenlampe | 2–3 h | | **Summe** | **~8 h** | --- ## Entscheidungen Am 2026-08-10 festgelegt: 1. **Tod:** Ausrüstung wird beim Respawn abgenommen, die Flasche bleibt mit ihrer Restluft im Inventar. → Phase 1: Death-Handler ruft `takeOffSuit()` ohne Progressbar, Phase 2 schreibt vorher die Restluft in die Metadata. 2. **Taschenlampe:** fest an der Ausrüstung, kein eigenes Item. An/Aus per Keybind. → Phase 5: Torch-Prop wird zusammen mit Maske und Flasche angehängt. 3. **Batterie:** keine. Lampe läuft, solange Sauerstoff da ist. → `metadata.battery` entfällt komplett, HUD zeigt nur An/Aus. 4. **Tiefenabhängiger Verbrauch:** an per Default, in der Config abschaltbar. → Phase 3: `decayRate * (1 + depth / 30)`, braucht den Tiefenwert aus Phase 4. Deshalb Phase 4 vor dem Feintuning der Verbrauchskurve fertigstellen. --- ## Offen Ingame getestet (2026-08-11): Anlegen/Ablegen, Flaschenauswahl, Luftverbrauch, HUD mit Tiefe und Druck, Lampe. Zwei Bugs kamen dabei raus und sind gefixt — `consume = 0` im Item-Snippet (der qbx-Callback feuerte nie) und leere Metadata bei frisch gekauften Flaschen. 1. **Scuba-Lampe braucht die Scuba-Kleidung** (Component 8). Auf Wunsch des Users fasst die Resource das Outfit nicht an — wer keine trägt, bekommt eine Meldung. Soll die Ausrüstung die Kleidung selbst setzen, muss das inklusive Sichern und Wiederherstellen gebaut werden (wie in wobozkyng/esx_scuba). 2. **`IsHudHidden()`** — ungetestet, ob die Native auf `DisplayHud(false)` anspringt. Falls nicht, greifen Taste und Export weiterhin. 3. **Auto-Anziehen nach Relog** und **HUD-Position in der Config** sind nicht umgesetzt. 4. **Balancing:** `maxDrainPerSecond = 5.0` muss über `maxDecayFactor = 4.0` liegen, sonst bekommen ehrliche Spieler in großer Tiefe vom Server Luft zurückgeschrieben. Wer den Tiefenfaktor hochdreht, muss beides nachziehen. Die Laufzeiten selbst sind noch nicht am Spielgefühl geprüft. --- ## Testplan - Anlegen/Ablegen an Land, im flachen Wasser, unter Wasser - Emote starten (rpemotes o.ä.) → Props bleiben - Ragdoll (aus Höhe fallen, angefahren werden) → Props bleiben - Kleiderladen / Skin-Change → Props hängen am neuen Ped - Relog mit halbvoller Flasche → Restluft stimmt - Resource-Restart während des Tauchens → Gear wird wiederhergestellt - Zwei Spieler, beide mit Lampe → jeder sieht das Licht des anderen - Lampe an, senkrecht nach unten tauchen → Kegel folgt der Blickrichtung nach unten (das ist der Fall, an dem `GetEntityForwardVector` gescheitert wäre) - Tank leer unter Wasser → Scuba aus, Ertrinken setzt ein - Mehrere Flaschen im Inventar → Auswahlmenü mit Füllstand pro Flasche - Volle Flasche auffüllen → Meldung, Kartusche wird **nicht** verbraucht - Flasche im Inventar ansehen → durability-Balken zeigt den Füllstand ```