Skip to content

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, or heartbeat (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 ​

ControlEffect
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 ​

lua
-- 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
FieldTypeNotes
onbooleanAnything but true with a valid direction reads as off
dx, dy, dznumberQuantised direction. Non-finite (NaN, inf) or zero vectors read as off
luxnumberIntensity, clamped to 0–100. Missing reads as 100
beam'wide'Only sent when wide. Missing or any other value reads as 'narrow'
strobetrueOnly 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 L again
  • 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 air crew member in a camera seat of that heli (crash, seat change, role lost), or whose heli is empty. The same sweep releases stale helicam_cam bags.

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).

np_* FiveM resources