Searchlight sync
The searchlight uses one entity state bag per helicopter, helicam_light. The operator's client never writes it. It sends the light state to the server, which checks it and writes the bag. v2 adds a beam width and a strobe to the same payload.
How it works
operator client server every client (incl. operator)
─────────────── ────── ─────────────────────────────
aim (camera forward when np_helicam:light:state AddStateBagChangeHandler('helicam_light')
slaved, or nose-down) → rate limit (≤ 10/s) → store payload per bag
→ LightSync.shouldSend? → air role + camera seat render thread
→ TriggerServerEvent( of a whitelisted heli? → only Config.Helicopters models
'np_helicam:light:state') → LightSync.unpack → nearest 4 within maxDist
→ Entity(heli).state:set( → slerp, beam shape, strobe phase
'helicam_light', …) → DrawSpotLight(WithShadow)Why the server writes it. A client can only write state bags on entities it network-owns, which is usually the pilot, so a copilot's light would not replicate. And a client-written bag can be faked by anyone. With the server relay, the copilot's light works and every payload has been checked.
Send side (operator). A send happens only if all of these are true:
- at least
minSendInterval(100 ms) since the last send, so at most 10 Hz - and one of: the direction moved more than
angleThreshold(1.5°), LUX, beam or strobe changed, orheartbeat(5 s) has passed
The direction is quantised before it is sent. The beam follows the camera's line of sight when the operator's Light follows cam setting is on (the default, shown as SLAVED on the HUD). When it is off (FIXED), or the camera is closed, the light points about 30° down from the nose.
Server. The server accepts an "on" payload only from a player with the air role in a camera seat of a whitelisted helicopter (its own view of the seat, not the client's). It unpacks it through LightSync.unpack, packs it again from the validated values and writes the bag. One operator owns a heli's light: the last valid sender takes it over, and only the owner's "off" clears it. An operator who changes helis without turning the light off leaves nothing burning.
Read side (everyone). The change handler only records the payload, because the entity often isn't streamed in yet. A render thread resolves the entity, ignores anything that isn't a model from Config.Helicopters, and draws the nearest four lights:
- within
shadowDist(250 m) of the local player:DrawSpotLightWithShadow - within
maxDist(800 m):DrawSpotLight - beyond that: nothing
The direction is smoothed with slerpRate, so remote beams glide between updates instead of snapping. With no lit heli in range, the thread checks every 500 ms.
Beam and strobe
| Control | Effect |
|---|---|
J (helicam_beam) | Cycles narrow ↔ wide. Each receiver draws the cone with Config.Searchlight.beams[beam].radius / .falloff |
helicam_strobe (no default key) | Toggles the strobe. Receivers blink the beam at Config.Searchlight.strobeHz (4 Hz) |
Both only work while the light is on and show a short toast. Turning the light off also turns the strobe off, so it doesn't start flashing the next time. Each client computes the blink from its own clock, so the blink rate matches across clients but the phase may not.
Payload
-- Entity(heli).state.helicam_light
{ on = true, dx = 0.12, dy = -0.98, dz = -0.15, lux = 80 } -- narrow, steady (v1 shape)
{ on = true, dx = 0.12, dy = -0.98, dz = -0.15, lux = 80, beam = 'wide', strobe = true }
{ on = false } -- off| Field | Type | Notes |
|---|---|---|
on | boolean | Anything but true with a valid direction reads as off |
dx, dy, dz | number | Quantised direction. Non-finite (NaN, inf) or zero vectors read as off |
lux | number | Intensity, clamped to 0–100. Missing reads as 100 |
beam | 'wide' | Only sent when wide. Missing or any other value reads as 'narrow' |
strobe | true | Only sent when on. Anything but true reads as off |
The client's event and the bag use the same shape, and both go through shared/lightsync.lua (LightSync.pack / LightSync.unpack): the client packs, the server unpacks and re-packs, and receivers unpack again. The defaults are left off the wire, so a narrow, steady beam packs exactly like v1, and v1 payloads still decode. beam is whitelisted because it is used as a key into Config.Searchlight.beams.
Turning it off
The light is turned off when:
- the operator presses
Lagain - the operator loses the seat, leaves the heli or moves to another heli
- the operator disconnects (the server clears every light that player owns)
- the resource stops. State bags outlive the resource, so the server clears every light it wrote.
- server sweep: every 10 s, the server turns off any lit bag whose owner is no longer an
aircrew member in a camera seat of that heli (crash, seat change, role lost), or whose heli is empty. The same sweep releases stalehelicam_cambags.
After a resource restart, the server doesn't know the owners of bags it wrote before. The sweep turns those lights off, and an operator who is still seated takes the light back with the next heartbeat (within 5 s).