From d2f6e3cd30ad3c0da7c707bb73549a42eed08bfb Mon Sep 17 00:00:00 2001 From: D4rkst3r Date: Tue, 11 Aug 2026 08:45:02 +0200 Subject: [PATCH] feat(light): auf die spieleigene Scuba-Lampe umgestellt GTA bringt seit v1493 eine eigene Lampe an der Tauchausruestung mit: SetEnableScubaGearLight(ped, toggle) 0xEE2476B9EE4A094F IsScubaGearLightEnabled(ped) 0x88274C11CF0D866D Die sitzt an der richtigen Stelle, leuchtet in Blickrichtung und braucht weder Prop noch Offsets noch einen Draw-Thread. Damit faellt der komplette DrawSpotLight-Aufbau weg: Kegelwerte, Farbe, Schatten, Distanzfilter, Lichter-Limit, die bone-relative Richtungsrechnung und das Sortieren der Kandidaten pro Frame. client/light.lua schrumpft von ~110 auf ~55 Zeilen. Gefunden ueber wobozkyng/esx_scuba. - Der Flag ist ein lokaler Ped-Zustand, der Statebag bleibt also. Gesetzt wird er per AddStateBagChangeHandler und zusaetzlich alle 500ms nachgezogen, weil Peds beim Streaming, Respawn und Model-Wechsel neu erzeugt werden - Die Lampe haengt an der Scuba-Kleidung (Component 8). Das Outfit wird bewusst nicht angefasst; wer sie nicht traegt, bekommt beim Einschalten eine Meldung statt stiller Wirkungslosigkeit - /divelampcheck sagt, ob das aktuelle Outfit eine Lampe hat - Prop ist jetzt optional und per Default aus Co-Authored-By: Claude Opus 5 --- README.md | 74 ++++++++++++++-------------- client/light.lua | 121 ++++++++++++++-------------------------------- client/main.lua | 34 ++++++++----- config/client.lua | 73 +++++++++------------------- locales/de.json | 3 +- locales/en.json | 3 +- 6 files changed, 119 insertions(+), 189 deletions(-) diff --git a/README.md b/README.md index efcf495..7c905c7 100644 --- a/README.md +++ b/README.md @@ -172,44 +172,31 @@ FiveM* umlegbar. | Option | Default | Bedeutung | |---|---|---| | `lampKey` | `'l'` | Vorbelegung | -| `lampBone` | `12844` | Bone für Prop **und** Licht — derselbe, damit der Strahl aus der Lampe kommt. `28422` = Hand statt Kopf | -| `lampProp` | `'prop_cs_police_torch'` | Optik. Ungültiges Model → Warnung im Log, Licht funktioniert trotzdem. `''` = kein Prop | -| `lampPropOffset` / `lampPropRotation` | `vec3(0,0,0)` | Sitz des Props am Bone — mit `/divelamp` einstellen | -| `lampOffset` | `vec3(0,0,0)` | Ursprung des Lichtkegels, bone-relativ | -| `lampDirection` | `vec3(0, 1, 0)` | Richtung des Kegels, bone-relativ | -| `lampColour` | `{255, 250, 235}` | RGB | -| `lampDistance` / `lampBrightness` / `lampRoundness` / `lampRadius` / `lampFalloff` | `28/3/0/16/2` | Form des Lichtkegels, Werte aus `d4rk_phone` übernommen | -| `lampShadows` | `false` | `true` nutzt `DrawSpotLightWithShadow` — sieht besser aus, kostet aber deutlich Performance | -| `lampDrawDistance` | `45.0` | Ab hier werden fremde Lampen nicht mehr gezeichnet | -| `lampMaxLights` | `4` | Gleichzeitige Lampen, die nächstgelegenen gewinnen | +| `lampReconcileIntervalMs` | `500` | Wie oft der Lampen-Zustand auf die Peds in der Nähe nachgezogen wird | +| `lampProp` | `''` | Optionales Prop am Kopf, rein optisch. Die Lampe leuchtet unabhängig davon | +| `lampBone` / `lampPropOffset` / `lampPropRotation` | `12844` / `vec3(0,0,0)` | Sitz des optionalen Props — mit `/divelamp` einstellen | > **Beim Balancing beachten:** `maxDrainPerSecond` (shared) muss über > `maxDecayFactor` (client) liegen. Sonst hält der Server den ehrlichen Verbrauch > in großer Tiefe für Cheat und schreibt Luft zurück. -## Lampe einstellen +## Debug-Befehle -Wo Prop und Lichtkegel am Kopf sitzen, hängt daran, wie das Prop-Model selbst -ausgerichtet ist — das lässt sich nicht am Schreibtisch erraten. Mit -`debug = true` in `config/client.lua` gibt es zwei Befehle, die die Werte im -laufenden Spiel ändern und die fertige Config-Zeile ins Log schreiben: +Mit `debug = true` in `config/client.lua`: ``` -/divelamp Sitz des Lampen-Props -/divelight Ursprung und Richtung des Lichtkegels +/divelampcheck Hat das aktuelle Outfit eine Scuba-Lampe? +/divelamp Sitz des optionalen Lampen-Props ``` -Das Prop wird sofort neu angehängt, das Licht zieht ohne Verzögerung mit — kein -Resource-Restart pro Versuch. Sind die Werte gut, die ausgegebenen Zeilen in die -Config übernehmen und `debug` wieder ausschalten. +`/divelamp` hängt das Prop sofort neu an und schreibt die fertige Config-Zeile ins +Log — kein Resource-Restart pro Versuch. Alternativ mit einem Prop-Attach-Editor +wie [noted_propattacher](https://github.com/NotedDevelopment/noted_propattacher) +ausmessen. -Alternativ mit einem Prop-Attach-Editor wie -[noted_propattacher](https://github.com/NotedDevelopment/noted_propattacher) -ausmessen: Prop spawnen, mit der Maus an die Stelle ziehen, Werte übernehmen. - -> Im Basegame gibt es **keine** Tauchlampe. `prop_cs_police_torch` ist eine -> Polizei-Taschenlampe; Varianten davon sind `prop_police_torch` und -> `prop_scn_police_torch`. Wer eine echte Tauchlampe will, braucht ein Custom-Model. +> Ein Tauchlampen-Prop gibt es im Basegame nicht. Was es gibt, sind Varianten +> derselben Polizei-Taschenlampe: `prop_cs_police_torch`, `prop_police_torch`, +> `prop_scn_police_torch`. Für die Beleuchtung braucht es das Prop ohnehin nicht. ## Exports @@ -222,18 +209,31 @@ Gedacht für HUD- und Screenshot-Resources. Zusätzlich folgt das HUD automatisc `IsHudHidden()`, greift also bei allem was `DisplayHud(false)` setzt, ohne dass etwas angebunden werden muss. -## Wie die Lampe synchron wird +## Wie die Lampe funktioniert -Licht ist in GTA nie synchronisiert — `DrawSpotLight` zeichnet nur lokal für den -aktuellen Frame. Der Lampenzustand wird deshalb als Statebag repliziert -(`LocalPlayer.state:set('divelight', …, true)`), und jeder Client zeichnet die -Lampen aller Taucher in seiner Nähe selbst. +Es wird kein Licht selbst gezeichnet. GTA bringt seit v1493 (Cayo Perico) eine +eigene Lampe an der Tauchausrüstung mit: -Die Richtung kommt aus **zwei bone-relativen Punkten** am Kopf-Bone, nicht aus -`GetEntityForwardVector`: beim Tauchen liegt der Ped waagerecht im Wasser, der -Entity-Forward würde stur horizontal leuchten. Die Offsets von `GetPedBoneCoords` -sind laut Native-Doku *"relative to the bone's rotation"*, die Differenz zweier -solcher Punkte enthält also den Pitch. +```lua +SetEnableScubaGearLight(ped, toggle) -- 0xEE2476B9EE4A094F +IsScubaGearLightEnabled(ped) -- 0x88274C11CF0D866D +``` + +Die sitzt an der richtigen Stelle, leuchtet in Blickrichtung und braucht weder +Prop noch Offsets noch einen Draw-Thread. + +> **Voraussetzung:** Die Lampe hängt an der echten **Scuba-Kleidung** des Peds +> (Component 8). Trägt der Spieler die nicht, tut die Native nichts. Diese +> Resource fasst das Outfit bewusst **nicht** an — das gehört ins Kleidungs-System. +> Beim Einschalten ohne passendes Outfit gibt es eine Meldung statt stiller Wirkungslosigkeit. +> Mit `debug = true` sagt `/divelampcheck`, ob das aktuelle Outfit eine Lampe hat. + +Der Flag ist ein **lokaler** Ped-Zustand und wird nicht von selbst repliziert. +Deshalb läuft der Zustand über einen Statebag +(`LocalPlayer.state:set('divelight', …, true)`), und jeder Client setzt den Flag +für die Taucher in seiner Nähe selbst — sofort per `AddStateBagChangeHandler`, +plus ein Abgleich alle `lampReconcileIntervalMs`, weil Peds beim Streaming, +Respawn und Model-Wechsel neu erzeugt werden und den Flag dabei verlieren. ## Entwicklung diff --git a/client/light.lua b/client/light.lua index bc500bc..568d4d1 100644 --- a/client/light.lua +++ b/client/light.lua @@ -1,111 +1,60 @@ local config = require 'config.client' --- Reiner Renderer: zeichnet die Lampen aller Taucher in der Nähe, gesteuert --- ausschließlich über den replizierten Statebag. Kein gemeinsamer Zustand mit --- client/main.lua - das hier läuft auch für Spieler, deren Ausrüstung wir gar --- nicht kennen. +-- Tauchlampe über die spieleigene Scuba-Lampe. -- --- Licht ist in GTA nie synchronisiert: DrawSpotLight zeichnet nur lokal, für den --- aktuellen Frame. "Gesynct" heißt deshalb, dass jeder Client das Licht jedes --- anderen Tauchers selbst zeichnet. +-- GTA bringt seit v1493 (Cayo Perico) eine eigene Lampe an der Tauchausrüstung mit: +-- SetEnableScubaGearLight(ped, toggle) 0xEE2476B9EE4A094F +-- IsScubaGearLightEnabled(ped) 0x88274C11CF0D866D +-- +-- Die sitzt an der richtigen Stelle, leuchtet in Blickrichtung und braucht weder +-- Prop noch Offsets noch einen eigenen Draw-Thread. +-- +-- WICHTIG: Die Lampe hängt an der echten Scuba-Kleidung des Peds (Component 8). +-- Trägt der Spieler die nicht, tut die Native nichts - dieses Script fasst das +-- Outfit bewusst nicht an. +-- +-- Der Flag ist ein lokaler Ped-Zustand und wird nicht von selbst repliziert. +-- Deshalb wie vorher der Umweg über den Statebag: jeder Client setzt den Flag für +-- jeden Taucher in seiner Nähe selbst. local STATE_KEY = 'divelight' ---- Kandidaten des aktuellen Frames. Wird wiederverwendet statt jedes Frame neu ---- angelegt, damit der GC bei Dauerbetrieb nichts zu tun bekommt. -local candidates = {} +--- @param ped number +--- @param enabled boolean +local function applyLight(ped, enabled) + if ped == 0 or not DoesEntityExist(ped) then return end ---- Position und Richtung der Lampe eines Peds. ---- ---- Die Offsets von GetPedBoneCoords sind laut Native-Doku "relative to the bone's ---- rotation" - zwei bone-relative Punkte ergeben also die echte Blickrichtung ---- inklusive Pitch. Genau das wird beim Tauchen gebraucht: der Ped liegt waagerecht ---- im Wasser, GetEntityForwardVector würde die Lampe stur horizontal leuchten lassen. -local function getLampVectors(ped) - local offset = config.lampOffset - local direction = config.lampDirection + -- Nur setzen wenn es sich tatsächlich ändert. Die Native ist billig, aber der + -- Aufruf pro Ped und Durchlauf wäre trotzdem unnötig. + if IsScubaGearLightEnabled(ped) == enabled then return end - local from = GetPedBoneCoords(ped, config.lampBone, offset.x, offset.y, offset.z) - local ahead = GetPedBoneCoords(ped, config.lampBone, - offset.x + direction.x, - offset.y + direction.y, - offset.z + direction.z) - - local delta = ahead - from - local length = #delta - - if length <= 0 then return end - - return from, delta / length + SetEnableScubaGearLight(ped, enabled) end -local function drawLamp(ped) - local from, direction = getLampVectors(ped) +--- Reagiert sofort auf Änderungen, auch von entfernten Spielern. +AddStateBagChangeHandler(STATE_KEY, nil, function(bagName, _, value) + local player = GetPlayerFromStateBagName(bagName) - if not from then return end + if player == 0 then return end - local colour = config.lampColour - - if config.lampShadows then - DrawSpotLightWithShadow(from.x, from.y, from.z, direction.x, direction.y, direction.z, - colour[1], colour[2], colour[3], - config.lampDistance, config.lampBrightness, config.lampRoundness, - config.lampRadius, config.lampFalloff, 0) - else - DrawSpotLight(from.x, from.y, from.z, direction.x, direction.y, direction.z, - colour[1], colour[2], colour[3], - config.lampDistance, config.lampBrightness, config.lampRoundness, - config.lampRadius, config.lampFalloff) - end -end + applyLight(GetPlayerPed(player), value == true) +end) +--- Der Statebag-Handler allein reicht nicht: Peds werden beim Streaming neu +--- erzeugt, beim Respawn ausgetauscht und beim Model-Wechsel ersetzt - der Flag +--- ist dann wieder weg, ohne dass sich der Statebag geändert hat. Deshalb +--- regelmäßig nachziehen. CreateThread(function() while true do local players = GetActivePlayers() - local myCoords = GetEntityCoords(cache.ped) - local count = 0 for i = 1, #players do local player = players[i] local state = Player(GetPlayerServerId(player)).state - if state and state[STATE_KEY] then - local ped = GetPlayerPed(player) - - if ped ~= 0 and DoesEntityExist(ped) then - local distance = #(myCoords - GetEntityCoords(ped)) - - if distance < config.lampDrawDistance then - count += 1 - local entry = candidates[count] - - if entry then - entry.ped, entry.distance = ped, distance - else - candidates[count] = { ped = ped, distance = distance } - end - end - end - end + applyLight(GetPlayerPed(player), state and state[STATE_KEY] == true) end - -- Reste des letzten Frames abschneiden, sonst sortiert table.sort veraltete - -- Einträge mit und ein alter, näherer Eintrag verdrängt einen echten. - for i = #candidates, count + 1, -1 do - candidates[i] = nil - end - - -- Nur sortieren wenn tatsächlich mehr Lampen da sind als gezeichnet werden - -- dürfen. DrawSpotLightWithShadow ist teuer, in einer vollen Höhle sonst - -- der erstbeste Frame-Killer. - if count > config.lampMaxLights then - table.sort(candidates, function(a, b) return a.distance < b.distance end) - end - - for i = 1, math.min(count, config.lampMaxLights) do - drawLamp(candidates[i].ped) - end - - Wait(count > 0 and 0 or 300) + Wait(config.lampReconcileIntervalMs) end end) diff --git a/client/main.lua b/client/main.lua index d7912cd..fc824d6 100644 --- a/client/main.lua +++ b/client/main.lua @@ -144,12 +144,22 @@ local function attachGear(ped) end --- @param state boolean -local function setLamp(state) +--- @param notifyIfUnavailable boolean? Rückmeldung, wenn das Outfit keine Lampe hat +local function setLamp(state, notifyIfUnavailable) if not currentGear.enabled then state = false end lampOn = state - -- Repliziert: client/light.lua zeichnet daraus die Lampen aller Taucher. + -- Repliziert: client/light.lua setzt daraus den Lampen-Flag bei allen Tauchern. LocalPlayer.state:set('divelight', state, true) + + -- Lokal sofort setzen statt auf den Abgleich-Thread zu warten. + SetEnableScubaGearLight(cache.ped, state) + + -- Die Spiel-Lampe hängt an der Scuba-Kleidung (Component 8). Fehlt die, tut + -- die Native nichts - ohne Hinweis sieht das nach einem kaputten Keybind aus. + if state and notifyIfUnavailable and not IsScubaGearLightEnabled(cache.ped) then + exports.qbx_core:Notify(locale('error.no_lamp_on_outfit'), 'error') + end end local function isPropIntact(prop) @@ -532,7 +542,7 @@ lib.addKeybind({ onPressed = function() if not currentGear.enabled then return end - setLamp(not lampOn) + setLamp(not lampOn, true) end }) @@ -575,17 +585,15 @@ if config.debug then lib.print.info(('lampPropOffset = %s,\nlampPropRotation = %s,'):format(fmtVec(offset), fmtVec(rotation))) end, false) - RegisterCommand('divelight', function(_, args) - local offset, direction = readVec(args, 1), readVec(args, 4) + --- Die Spiel-Lampe braucht die Scuba-Kleidung am Ped. Dieser Befehl sagt, ob + --- das aktuelle Outfit sie überhaupt hat - sonst rätselt man, warum nichts + --- leuchtet. + RegisterCommand('divelampcheck', function() + SetEnableScubaGearLight(cache.ped, true) + local works = IsScubaGearLightEnabled(cache.ped) + SetEnableScubaGearLight(cache.ped, lampOn) - if not offset or not direction then - lib.print.info('/divelight - Ursprung und Richtung des Lichtkegels') - return - end - - config.lampOffset, config.lampDirection = offset, direction - - lib.print.info(('lampOffset = %s,\nlampDirection = %s,'):format(fmtVec(offset), fmtVec(direction))) + lib.print.info(('Scuba-Lampe am aktuellen Outfit: %s'):format(works and 'ja' or 'nein')) end, false) end diff --git a/config/client.lua b/config/client.lua index e2f73cf..865d176 100644 --- a/config/client.lua +++ b/config/client.lua @@ -66,58 +66,29 @@ return { --- Tastenbelegungen unter "FiveM" umlegen. lampKey = 'l', - --- Bone, an dem Lampe und Licht hängen. Beides derselbe Bone, damit der - --- Strahl auch sichtbar aus der Lampe kommt. - --- 12844 = Kopf (wie die Maske) - Strahl folgt der Blickrichtung - --- 28422 = Hand (wie das Prop in d4rk_phone) - Handlampe, aber beim - --- Schwimmen sind die Arme seitlich ausgestreckt, der Kegel - --- wandert also mit den Schwimmzügen + --- Die Lampe ist die spieleigene Scuba-Lampe (SetEnableScubaGearLight, seit + --- v1493). Sie sitzt an der richtigen Stelle und leuchtet in Blickrichtung - + --- deshalb gibt es hier weder Offsets noch Farb- und Kegelwerte. + --- + --- ACHTUNG: Sie hängt an der echten Scuba-Kleidung des Peds (Component 8). + --- Trägt der Spieler die nicht, passiert nichts. Diese Resource fasst das + --- Outfit bewusst nicht an - das macht bei Bedarf das Kleidungs-System. + + --- Wie oft der Lampen-Zustand auf die Peds in der Nähe nachgezogen wird (ms). + --- Nötig, weil Peds beim Streaming, Respawn und Model-Wechsel neu erzeugt + --- werden und den Flag dabei verlieren. + lampReconcileIntervalMs = 500, + + --- Optionales Prop am Kopf, rein optisch. Die Lampe leuchtet unabhängig davon. + --- Standardmäßig aus, weil die Spiel-Lampe schon an der Ausrüstung sitzt. + --- Ein Basegame-Tauchlampen-Prop gibt es nicht; die Polizei-Taschenlampe + --- ('prop_cs_police_torch', 'prop_police_torch', 'prop_scn_police_torch') + --- passt optisch nur bedingt. + lampProp = '', + + --- Bone und Sitz des optionalen Props. 12844 = Kopf (wie die Maske). + --- Mit /divelamp im Spiel einstellen statt hier zu raten. lampBone = 12844, - - --- Prop am Kopf. Rein optisch - das Licht hängt nicht daran, es wird nur - --- vom selben Bone aus gezeichnet. Ist das Model ungültig, leuchtet es - --- trotzdem und es gibt eine Warnung im Log. - --- Im Basegame gibt es keine Tauchlampe. Alternativen sind Varianten - --- derselben Polizei-Lampe: 'prop_police_torch', 'prop_scn_police_torch'. - --- Auf '' setzen = gar kein Prop, nur Licht. - lampProp = 'prop_cs_police_torch', - - --- Sitz des Props am Bone. Wie in d4rk_phone bewusst auf null: das Prop sitzt - --- dann genau im Bone statt auf geratenen Offsets. Beim Kopf-Bone darf ein - --- kleiner Versatz nach vorne/seitlich nötig sein, damit die Lampe nicht im - --- Schädel steckt - mit /divelamp im Spiel einstellen statt hier zu raten. lampPropOffset = vec3(0.0, 0.0, 0.0), lampPropRotation = vec3(0.0, 0.0, 0.0), - - --- Ursprung des Lichtkegels relativ zum Bone (bone-relativ, nicht Welt). - --- Sollte ungefähr dort liegen, wo das Prop sitzt. - lampOffset = vec3(0.0, 0.0, 0.0), - - --- Richtung des Lichtkegels, ebenfalls bone-relativ. Leuchtet der Kegel in die - --- falsche Richtung, hier die Achse tauschen oder das Vorzeichen drehen - - --- welche lokale Achse des Kopf-Bones nach vorne zeigt, hängt am Skelett. - lampDirection = vec3(0.0, 1.0, 0.0), - - --- Lichtwerte aus d4rk_phone übernommen - dort im Betrieb erprobt, statt hier - --- neu geraten. Leicht warmweiß wie eine echte Taschenlampe. - lampColour = { 255, 250, 235 }, - - lampDistance = 28.0, -- Reichweite in Metern - lampBrightness = 3.0, -- Helligkeit - lampRoundness = 0.0, -- Weichzeichnung des Kegelrands - lampRadius = 16.0, -- Öffnungswinkel - lampFalloff = 2.0, -- Abfall zum Rand hin - - --- Schatten kosten deutlich Performance. d4rk_phone nutzt bewusst das - --- schattenlose DrawSpotLight, deshalb hier auch der Default - unter Wasser - --- fällt der Unterschied kaum auf, in einer Höhle mit mehreren Tauchern - --- dagegen sehr wohl. - lampShadows = false, - - --- Ab dieser Entfernung werden fremde Lampen nicht mehr gezeichnet (Meter). - lampDrawDistance = 45.0, - - --- Wie viele Lampen gleichzeitig gezeichnet werden. Die nächstgelegenen - --- gewinnen. Schützt vor Frame-Einbrüchen bei Gruppentauchgängen. - lampMaxLights = 4, } diff --git a/locales/de.json b/locales/de.json index f1b8fc2..30eaa0a 100644 --- a/locales/de.json +++ b/locales/de.json @@ -6,7 +6,8 @@ "no_tank": "Du hast keine Tauchflasche zum Auffüllen dabei!", "tank_empty": "Diese Tauchflasche ist leer!", "tank_already_full": "Diese Tauchflasche ist bereits voll!", - "out_of_air": "Deine Luft ist alle! Sofort auftauchen!" + "out_of_air": "Deine Luft ist alle! Sofort auftauchen!", + "no_lamp_on_outfit": "Deine Tauchausrüstung hat keine Lampe." }, "success": { "took_out": "Du hast deine Ausrüstung abgelegt!", diff --git a/locales/en.json b/locales/en.json index 34d1c34..44fe2af 100644 --- a/locales/en.json +++ b/locales/en.json @@ -6,7 +6,8 @@ "no_tank": "You don't have an air tank to refill!", "tank_empty": "This air tank is empty!", "tank_already_full": "This air tank is already full!", - "out_of_air": "You're out of air! Surface now!" + "out_of_air": "You're out of air! Surface now!", + "no_lamp_on_outfit": "Your diving suit doesn't have a lamp." }, "success": { "took_out": "You've taken your gear off!",