Files
d4rk_divegear/PLAN.md
T
D4rkst3randClaude Opus 5 921d9c89c8
Lint / Lint Resource (push) Has been cancelled
fix(client): Ausruestung verschwindet nicht mehr bei Animationen
Die Props waren soft-pinned angehaengt (useSoftPinning = true), wodurch die
Physik-Engine die Attachment bei Ragdoll und harten Anim-Uebergaengen loesen
darf. Dazu kam, dass ein Ped-Model-Wechsel und aufraeumende Fremd-Scripts
(Emotes, Handcuffs) gar nicht abgefangen wurden.

- attachProp() Helper mit harter Attachment (useSoftPinning = false)
- Watchdog-Thread prueft jede Sekunde, ob beide Props noch am aktuellen Ped
  haengen, und haengt sie sonst neu an
- Scuba-Flags werden im Watchdog neu gesetzt, weil das Spiel sie bei Respawn
  und Fahrzeugausstieg zuruecksetzt
- lib.onCache('ped') haengt die Props nach einem Model-Wechsel neu an
- Ausruestung wird beim Tod abgenommen (removeGearOnDeath, default true)
- removeGear() als gemeinsamer Teardown fuer Ablegen, leere Flasche und Tod
- onResourceStop raeumt Props und Scuba-Flags auf
- maxTimeUnderwater / maxTimeUnderwaterOutOfAir statt Magic Numbers

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 23:24:39 +02:00

9.9 KiB
Raw Blame History

d4rk_divegear — Umbauplan

Fork von 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 35 bauen darauf auf.

Phase 1 — Gear-Persistenz (Bugfix, ~1 h)

Zuerst, weil kleinster Aufwand, echter Bug, und die Taschenlampe dieselbe Attach-Logik nutzt.

  • attachGear() auf harte Attachment umstellen: useSoftPinning = false, p9 = false, fixedRot = true — ausgelagert in einen attachProp()-Helper, den Phase 5 für die Lampe mitbenutzt
  • isGearIntact() Helper: DoesEntityExist + GetEntityAttachedTo(...) == cache.ped für beide Props
  • 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)
  • lib.onCache('ped', ...) → bei Model-Wechsel (Kleiderladen, Skin-Change) Props neu anhängen, Scuba-State neu setzen
  • Death-Handler: IsPedDeadOrDying-Check im Watchdog statt eigenem Event — keine Abhängigkeit zu qbx_medical o.ä. Sichern der Restluft kommt in Phase 2 dazu
  • Gemeinsamer removeGear(outOfAir)-Teardown für Ablegen, leere Flasche und Tod
  • 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)

  • Umstellung von "Punkte" auf Restsekunden (oxygenSeconds)
  • Decrementer-Thread auf GetGameTimer()-Delta statt gezählter Wait(1000) (driftfrei, überlebt Frame-Drops)
  • Float-Vergleiche raus, Warnschwellen explizit als Config-Liste (z.B. warnAtSeconds = { 60, 30, 10 }), jede Schwelle einmal auslösen
  • Anzeige als mm:ss statt roher Zahl
  • 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)
  • Beim Anlegen Metadata lesen → Restluft übernehmen
  • Beim Spawn/Resource-Start: war die Gear angelegt, direkt wieder anziehen

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
  • Items in config.lua definieren (Label, capacity, Gewicht, Prop-Scale)
  • items_for_ox_inventory.lua als Snippet-Datei zum manuellen Reinkopieren in ox_inventory/data/items.lua — kein RegisterItem-Export
  • metadata.oxygen (Restsekunden) + metadata.durability (Prozent, damit der Füllstand direkt im Inventar-Slot sichtbar ist)
  • diving_fill füllt bis capacity des jeweiligen Tanks statt auf Fixwert
  • 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):

  • Tiefe = Wasseroberfläche Ped-Z, via GetWaterHeight (Fallback GetWaterHeightNoWaves)
  • Wert geglättet (Lerp) damit die Zahl nicht zappelt
  • Liefert gleichzeitig den Faktor für den tiefenabhängigen Verbrauch aus Phase 3

NUI (~2 h):

  • O2 als Balken/Ring + mm:ss Restzeit, Farbwechsel an den Warnschwellen aus Phase 2, Pulsieren im kritischen Bereich
  • Tiefe in Metern, Warnfarbe ab Config-Tiefe
  • Slot für den Lampen-Status (wird in Phase 5 befüllt)
  • Sichtbar nur wenn Gear angelegt und unter Wasser, mit Fade-In/Out
  • Updates gedrosselt per SendNUIMessage (~4/s statt jeden Frame)
  • Kein backdrop-filter (bekannter FiveM-Bug → schwarzer Hintergrund)
  • Position/Scale in config/client.lua einstellbar

Phase 5 — Gesyncte Taschenlampe (~23 h)

Licht ist in GTA nie synchronisiert — jeder Client zeichnet das Licht für jeden Taucher in seiner Nähe selbst.

  • Torch-Prop mit derselben harten Attach-Methode aus Phase 1 anhängen (Kopf-Bone oder Hand, Config)
  • Toggle per RegisterKeyMapping/lib.addKeybind
  • State replizieren: LocalPlayer.state:set('divelight', true, true) — Statebag statt Event-Spam
  • Draw-Thread: über GetActivePlayers(), Player(serverId).state.divelight prüfen, DrawSpotLightWithShadow zeichnen
  • Richtung aus dem Prop, nicht aus dem Ped: der Ped pitcht beim Tauchen, GetEntityForwardVector geht schief. Position + Richtung über GetOffsetFromEntityInWorldCoords(torchProp, 0, 1, 0) aus der Prop-Matrix
  • Fremde Props: einmal pro Sekunde GetGamePool('CObject') nach Torch-Model filtern, per GetEntityAttachedTo dem Ped zuordnen, Ergebnis cachen. Fallback wenn nicht gefunden: Kopf-Bone + Ped-Matrix
  • Performance: Distanzfilter ~4050 m, max. 34 gleichzeitige Lichter (DrawSpotLightWithShadow ist teuer)
  • 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.

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
│   ├── oxygen.lua           # Decrementer, Persistenz-Sync
│   ├── light.lua            # Statebag + Draw-Thread
│   └── hud.lua              # Tiefenberechnung + NUI-Bridge
├── 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 23 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.

Offene Fragen

  1. Gitea: Repo unter git.d4rkst3r.de/D4rkst3r/d4rk_divegear — soll ich das Remote vorbereiten? (Push machst du selbst.)

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
  • Tank leer unter Wasser → Scuba aus, Ertrinken setzt ein