TESTING.md war nicht filterbar, nicht zuweisbar und haette neben den Issues als zweite Quelle gedriftet. Jetzt ein Issue pro Testbereich mit Labels (test, prio:kritisch/mittel/niedrig, ungeprueft), plus bug und doku fuer spaeter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
13 KiB
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:
- Sauerstoff ist eine lokale Client-Variable (
local oxygenLevel = 0) — nicht persistent, nicht am Item hängend, weg bei Relog/Restart. Blockiert Flaschengrößen. - Laufzeit-Rechenfehler:
startingOxygenLevel = 100beidecayRate = 1pro Sekunde ergibt 100 Sekunden, nicht Minuten. Die VergleicheoxygenLevel % 10 == 0undoxygenLevel == 0(client/main.lua:121, :124) brechen, sobalddecayRatefraktional wird. - 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.
attachGear()auf harte Attachment umstellen:useSoftPinning = false,p9 = false,syncRot = true— ausgelagert in einenattachProp()-Helper, den Phase 5 für die Lampe mitbenutzt. Laut Native-Doku heißtuseSoftPinningfalse: "attached entity will not detach when fixed" — genau das wollen wirisGearIntact()Helper:DoesEntityExist+GetEntityAttachedTo(...) == cache.pedfür beide Props- Watchdog-Thread (
gearWatchdogIntervalMs, default 1000 ms) solangecurrentGear.enabled: bei DefektdeleteGear()+attachGear(), undenableScuba()neu setzen (SetEnableScuba/SetPedMaxTimeUnderwaterwerden 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 onResourceStopCleanup-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ählterWait(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:ssstatt 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
- 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 |
capacityinconfig/shared.lua(Client und Server brauchen sie), Label und Gewicht im Item-Snippet. Kein Prop-Scale — siehe Optik-Hinweis untenitems_for_ox_inventory.luaals Snippet-Datei zum manuellen Reinkopieren inox_inventory/data/items.lua— keinRegisterItem-Exportmetadata.oxygen(Restsekunden) +metadata.durability(Prozent, damit der Füllstand direkt im Inventar-Slot sichtbar ist)diving_fillfüllt biscapacitydes 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(FallbackGetWaterHeightNoWaves) - 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:ssRestzeit, 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) hudEnabled,hudUpdateIntervalMsunddeepWarningDepthinconfig/client.lua- 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). UnterreservePressurewird rot eingefärbt - Nachgereicht: HUD ausblendbar für Unterwasser-Screenshots. Drei Wege,
weil
qbx_coreselbst keinen HUD-Toggle hat (es feuert nurhud:client:*an eine separate HUD-Resource):IsHudHidden()folgen, eigene Taste, und der ExportsetHudVisible(bool) - Offen: Position und Größe des HUD sind bisher nur in
nui/style.csseinstellbar, 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. 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:
- 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.divelightprüfen,DrawSpotLightWithShadowzeichnen - Richtung anders gelöst als geplant: statt Prop-Matrix und Objekt-Pool
werden zwei bone-relative Punkte am Kopf-Bone benutzt. Die Offsets von
GetPedBoneCoordssind 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 (GetEntityForwardVectorwürde beim waagerecht im Wasser liegenden Ped stur horizontal leuchten), ohne fremde Props überGetGamePool('CObject')suchen und cachen zu müssen — der Renderer braucht dadurch überhaupt keinen Prop-Zugriff und kommt mit dem Statebag allein aus - Prop-Model wird über
IsModelValid/IsModelInCdimagegeprüft, weillib.requestModelbei ungültigen Models einen Error wirft. Ein falscher Prop-Name kostet so nur die Optik, nicht die ganze Ausrüstung - Performance: Distanzfilter ~40–50 m, max. 3–4 gleichzeitige Lichter
(
DrawSpotLightWithShadowist 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.
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:
- 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. - 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.
- Batterie: keine. Lampe läuft, solange Sauerstoff da ist.
→
metadata.batteryentfällt komplett, HUD zeigt nur An/Aus. - 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.
- 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).
IsHudHidden()— ungetestet, ob die Native aufDisplayHud(false)anspringt. Falls nicht, greifen Taste und Export weiterhin.- Auto-Anziehen nach Relog und HUD-Position in der Config sind nicht umgesetzt.
- Balancing:
maxDrainPerSecond = 5.0muss übermaxDecayFactor = 4.0liegen, 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
Als Issues im Repo, ein Issue pro Testbereich: git.d4rkst3r.de/D4rkst3r/d4rk_divegear/issues