A helicopter surveillance camera for police and air units in FiveM. It has a sensor HUD with three modes: day (EO), low-light (NV) and thermal (IR). You can lock onto a target and the camera follows it. If you raise the lock distance above the OneSync streaming range, the server keeps the lock tracking after the target streams out of the operator's range (see Features). The camera also looks up plates in your server's vehicle database. With pma-voice installed, it can listen to people on the ground. A searchlight lights the point the camera looks at, and every player nearby sees it.
It runs on qbx_core, qb-core, ESX Legacy or standalone (ACE permissions). Version 1.1.0.
Features
Camera
- The script camera is mounted on the helicopter, and each model can have its own mount position. Mouse pans and tilts. Zoom is smooth, from 1x to about 23x (FOV 70° to 3°).
- There are three sensor modes: EO (day), LLTV night vision and IR white-hot thermal. On exit, the player's previous night vision and thermal state comes back.
- The camera opens only in configured helicopter models, only from allowed seats and only at or above a minimum height above ground. It closes itself when the helicopter drops below that height, when the player leaves the aircraft or dies, and when the pause menu opens.
- A help text ("Press E to use the helicopter camera") shows while the camera can be opened.
- The HUD shows:
- agency and unit (callsign)
- compass band and heading
- sensor mode, zoom, FOV and gimbal elevation
- clock
- altitude ruler (AGL / MSL), position and ground speed
- zoom bar, NV / IR / LIGHT toggles and key hints
- a red warning banner
- The camera takes no NUI focus, so chat and other interfaces keep working.
Targeting
The crosshair identifies vehicles, persons and ground points.
Left click starts a lock. After
Config.Lock.secondsthe camera follows the target on its own. Left click again to release it, which takes the same time.A lock keeps tracking when the target streams out of the operator's client. The server then sends its position every 200 ms (OneSync). OneSync streams entities out at about 424 m, so this comes into play when you raise
Config.Camera.maxLockDistabove that (default 400 m) or your server uses a smaller culling radius.A lock breaks when:
- the target is deleted
- the target is farther than
Config.Camera.maxLockDist - a locked person gets into a vehicle
Walls and roofs never break a lock.
The target panel shows plate, model, owner, registration, distance and speed. WANTED and PLATE MISMATCH badges appear when they apply.
Plate lookup
The server reads the plate from the vehicle entity. It never takes a plate from the client. Then it looks the plate up:
- qb-core / qbx_core:
player_vehicles+players - ESX Legacy:
owned_vehicles+users
Common schema differences (job columns added by garage scripts, an owner join that does not work) are handled with fallback queries. Other schemas: adapt
server/editable.lua.- qb-core / qbx_core:
The panel tells apart private, organization (job / society), fleet and unregistered vehicles. Emergency-class vehicles are shown as government vehicles.
Open hooks connect your own tables for owner records, job fleets, wanted flags and fake plates.
Results are cached per plate. Each player is rate limited, and the server checks distance and routing bucket.
Audio surveillance (optional, pma-voice)
- When you lock a person or a ground point, the operator hears the players inside a circle 5 m across (2.5 m radius) around it.
- Voices sound like a radio feed. The farther the point is from the camera, the lower the volume and the more flutter and random "SIGNAL LOST" dropouts. Stronger radio distortion steps can be set up in the config (see What the operator hears).
- A line-of-sight check stops speakers behind walls, under roofs or inside interiors from being heard.
- The server does the range scan, and the listening point must be near the helicopter.
Searchlight
- G switches a searchlight on the point under the crosshair. While a lock is held, the light follows the target.
- The lit circle keeps its size at any range (by default 5 m across, up to 400 m from the camera). Every player nearby sees it, on the ground too. Shadows are optional.
Integration
- Client exports
IsActive()andIsSpotlightOn(), server exportIsUsingCamera(source). - Local client events
xr-helicam:openedandxr-helicam:closed, the state bagLocalPlayer.state.helicamand the replicated player statehelicamSpot(searchlight). - Open hooks on client and server for notifications, extra checks, hiding your HUD, callsigns and vehicle records.
Logging and localisation
- Rate-limited logs go to the server console and to a Discord webhook: camera opened, plate checks and audio locks.
- 14 languages, km/h or mph, meters or feet, and a configurable date format.
Requirements
| Requirement | Needed | Notes |
|---|---|---|
| OneSync | yes | Must be enabled on the server. Server-side tracking and every check use server entity natives. |
| ox_lib | yes | Callbacks and the default notifications. |
| oxmysql | yes | The manifest loads it on every setup, standalone included. In standalone mode the default configuration sends no queries. Standalone: oxmysql only has to be started. No database is needed, and a connection error that oxmysql prints because mysql_connection_string is not set does not affect xr-helicam. |
| Framework | no | qbx_core, qb-core or ESX Legacy. Without one, the script runs standalone with ACE permissions. |
| pma-voice | optional | Needed only for audio surveillance. Other voice resources are not supported. Without pma-voice, the audio feature turns itself off and the rest of the camera still works. |
The resource is protected by Cfx.re Asset Escrow. Your server's license key must belong to the Cfx.re account that bought xr-helicam.
Installation
Download xr-helicam from portal.cfx.re (Granted Assets). Use the Cfx.re account you bought it with.
Extract the
xr-helicamfolder into your resources, e.g.resources/[police]/xr-helicam. Do not rename the folder. Other scripts reach its exports asexports['xr-helicam'], and its events start withxr-helicam:.Add it to
server.cfgafter its dependencies:ensure ox_lib # required on standalone servers too (no database needed there) ensure oxmysql # your framework: qbx_core, qb-core or es_extended (none on standalone servers) ensure qbx_core # optional, only for audio surveillance ensure pma-voice ensure xr-helicamSet up access. See Permissions:
Config.AllowedJobsfor framework jobs, ACE for admins and standalone servers.Optional: add a Discord webhook. See Discord logging.
Restart the server. The console prints one ready line:
[xr-helicam] v1.1.0 ready | framework: qbx | helicopter models: 1 | discord log: offTest in game: sit in a
polmavas an on-duty police officer, climb above 5 m and press E. On qb-core / qbx_core servers with qb-policejob or qbx_policejob, first turn off their own helicopter camera (see Conflicts with other scripts). Otherwise one E press opens two cameras.
Do not keep backup copies inside the resource, and never in locales/. The manifest loads locales/*.lua, and FiveM can pick up copies like de.lua.bak or de - Copy.lua too. A copy loaded that way can overwrite the language you edited. Keep backups outside your resources folder.
Customers only need to edit config.lua, locales/*.lua, client/editable.lua and server/editable.lua. The HUD files in html/ are readable as well. After a change, restart the resource or the server.
Conflicts with other scripts
qb-policejob / qbx_policejob helicopter camera. These police jobs ship their own helicopter camera in client/heli.lua. It opens with E in a polmav for on-duty police: the same key and model as the xr-helicam defaults. Left as is, one E press opens both cameras. While xr-helicam is open, it blocks E and the right mouse button, so the other camera cannot be closed or switched. Its overlay can stay on screen after xr-helicam closes. Use one of these fixes:
- Turn the other camera off (recommended). In that resource's
fxmanifest.lua, remove the line'client/heli.lua',fromclient_scripts, then restart the server. If the manifest loads the folder with a wildcard such as'client/*.lua', moveclient/heli.luaout of your resources folder instead. The file can contain other helicopter features of that resource as well (in qb-policejob, the spotlight and rappel keys), so check it before you remove it. xr-helicam brings its own searchlight. - Move it to another model. Set the police helicopter model in that resource's config (qb-policejob:
Config.PoliceHelicopter, qbx_policejob:policeHelicopterin its client config) to a model that is not in the xr-helicamConfig.Helicopters. - Give xr-helicam another key. Change
Config.EnterKey(see Controls). Both cameras then stay installed, each on its own key. Do not open them at the same time. The spotlight of that resource also stays on G (control 183) in the same model, so one G press can switch both searchlights. Move the xr-helicam searchlight to another key (Config.Spotlight.key, e.g.{ control = 311, label = 'K' }, not your new open key), or add thehelicamstate bag check to that resource's spotlight key (see Key conflicts).
The same applies to any other helicopter camera script that uses E in the same models. Scripts that only have an E action of their own (dispatch, radial menus) need just one line: see Key conflicts.
Framework support
With Config.Framework = 'auto' (default), the script uses the first framework it finds running, in this order: qbx_core, qb-core, es_extended. If none is running, it runs standalone. To force one, set 'qbx', 'qb', 'esx' or 'standalone'. Always start xr-helicam after your framework.
| qbx_core | qb-core | ESX Legacy | Standalone | |
|---|---|---|---|---|
| Access | job name + job.grade.level |
job name + job.grade.level |
job.name + job.grade |
ACE only |
Duty (RequireDuty) |
job.onduty |
job.onduty |
job.onDuty if your ESX version has it, otherwise not checked |
- |
| HUD unit (callsign) | metadata.callsign |
metadata.callsign |
Editable.GetCallsign |
Editable.GetCallsign |
| Plate owner | player_vehicles + players |
player_vehicles + players |
owned_vehicles + users |
none (dashes), unless you fill Editable.FetchVehicleRecord |
| Vehicle name in the Discord log | GetVehiclesByName() label, else the spawn name |
QBCore.Shared.Vehicles label, else the spawn name |
none (owned_vehicles stores no model name) |
the model your Editable.FetchVehicleRecord returns |
| Operator in logs | character name + citizenid | character name + citizenid | character name + identifier | player name |
| Fake plate check | yes (opt-in) | yes (opt-in) | no | no |
| Instant close on job / duty change | QBCore:Server:OnJobUpdate, QBCore:Server:SetDuty |
same | esx:setJob |
- |
| Instant close on logout / character switch | QBCore:Server:OnPlayerUnload |
same | esx:playerLogout |
- |
Every open camera is also checked again every 5 s: the helicopter must still exist, the player must still be inside it and access must still be granted. Changes that fire no framework event, such as a removed ACE, are caught this way.
With Config.UseCallsign = true, Editable.GetCallsign (server/editable.lua) works on every framework and wins over the framework callsign. The unit falls back to Config.UnitFallback.
Permissions
The server decides access. The checks run in this order:
- ACE: a player with
Config.AcePermission(defaulthelicam.use) always passes. Job, grade, duty andEditable.CanUseCameraare skipped. - Standalone: nobody else passes.
- Job and grade: the job must be in
Config.AllowedJobs, with at least the grade set there. - Duty: with
Config.RequireDuty = true, the player must be on duty. - Custom rule:
Editable.CanUseCamera(src, job)must not returnfalse.
Opening the camera also needs the helicopter checks:
- the model is in
Config.Helicopters - the player is inside that helicopter
- the seat is allowed by
Config.AllowedSeats - the helicopter is at least
Config.MinAltitudeabove ground (checked by the client)
Config.AllowedJobs
Config.AllowedJobs = {
police = 0, -- grade 0 and higher = every grade
sheriff = 2, -- grade 2 and higher
['state-police'] = true, -- every grade (names with a dash need brackets)
}
- Use the framework's job name (not its label), with exact case.
- Grades are numbers:
job.grade.levelon qb/qbx,job.gradeon ESX. police = falsehas the same effect as leaving the job out.- Entries in another format are ignored, and the server console prints a warning at start. Examples: a list such as
{ 'police' }, or a grade written as text such aspolice = '2'.
Config.RequireDuty
When true, off-duty players are treated like players without access: the key does nothing and no help text shows. A camera that is open when its player goes off duty closes with "Your camera access was revoked." If the framework has no duty state (for example an ESX version without job.onDuty), duty is not checked.
ACE examples (server.cfg)
# Admins can always use the camera
add_ace group.admin helicam.use allow
# Standalone: create a group and add players to it
add_ace group.aircrew helicam.use allow
add_principal identifier.license:0123456789abcdef0123456789abcdef01234567 group.aircrew
- A player is in
group.adminonly if your server.cfg (or your admin tooling) adds them withadd_principal. Config.AcePermission = falseturns the ACE check off. On a standalone server nobody can then use the camera, and the console warns about it.
When access changes
The key and the help text only work for players with access. The client asks the server while the player sits in a camera helicopter and remembers the answer for 15 s. qb/qbx/ESX job, duty and character events reset that answer at once. After an ACE change, leave and re-enter the helicopter, or wait up to 15 s.
Controls
| Input | Action |
|---|---|
E (Config.EnterKey) |
Open the camera (allowed seat, at or above Config.MinAltitude). Press again to close. |
| ESC / Backspace | Close the camera |
| Mouse | Pan / tilt. Disabled while a lock is held. |
| Mouse wheel | Zoom in (up) / out (down) |
| Right mouse button | Next vision mode: EO > NV (LLTV) > IR (thermal, white hot) > EO |
| Left mouse button | Lock the target under the crosshair (acquiring takes Config.Lock.seconds). Click again while locked to release it (takes the same time). |
G (Config.Spotlight.key) |
Searchlight on / off (see Searchlight). |
- Input is ignored while chat or another interface has NUI focus. The camera stays open.
- While the camera is open, the GTA controls it needs are disabled: looking around, attack, aim, weapon wheel and weapon switching, vehicle weapons, melee, the E actions (
INPUT_PICKUP,INPUT_CONTEXT), the searchlight key (G,INPUT_DETONATE) and pause. Other scripts can still react to these keys (see Key conflicts). - To change the key, set
Config.EnterKey.controlto a control index number (not a letter; e.g.311for K) from the FiveM controls list. Setlabelto what the HUD should show andhelpTokento the same control'sINPUT_*name, which the help text uses to draw the key button. The chosen control is also disabled for GTA while the camera is open. - The searchlight key works the same way:
Config.Spotlight.key = { control = 311, label = 'K' }. It must differ from the open / close key, ESC (200) and Backspace (202); otherwise the searchlight has no key and F8 shows a warning. If you move the open key to G, move the searchlight to another key too.
HUD warnings
The red banner shows these messages (texts come from the language file):
| Warning | Cause |
|---|---|
| OUT OF RANGE | The target is farther than Config.Camera.maxLockDist when you try to lock it, or a held lock went beyond that distance. |
| LOCK LOST | The locked target was deleted or moved to another routing bucket, or its position was unknown for 1.5 s. |
| TARGET IN VEHICLE | A locked person got into a vehicle. The person lock ends. Lock the vehicle to follow it. |
| GIMBAL LIMIT | The locked target is outside Config.Camera.pitchMin / pitchMax. |
| ALTITUDE LOW | Less than 10 m above Config.MinAltitude. Below the minimum the camera closes. |
| AUDIO OUT OF RANGE | The server rejected the listening point: farther than Config.Audio.maxRangeFromHeli from the helicopter. |
| AUDIO UNAVAILABLE | The server rejected audio (turned off on the server, or invalid data). |
Configuration reference
config.lua is shared and sent to every client: never put secrets in it. The webhook belongs in server.cfg or in server/editable.lua. All distances in config.lua are meters and all speeds km/h. Config.Units only changes what the HUD displays.
General
| Option | Default | Description |
|---|---|---|
Config.Debug |
false |
Server console diagnostics: for each player in an allowed seat of a camera helicopter, whether camera access is granted and which check denies it (job, grade, duty, ACE, framework, Editable.CanUseCamera); see The camera does not open. Also prints failed framework calls (bridge errors) and adds the database message to the optional-column note of the plate queries. With the searchlight on, the operator sees a calibration ring on the lit point (see Searchlight), and searchlight debug lines appear (see Console messages). |
Config.Locale |
'en' |
Language of the HUD, notifications, help text and Discord logs. See Languages. |
Config.Units |
{ speed = 'kmh', altitude = 'm', distance = 'm' } |
HUD units. See Units and date. |
Config.DateFormat |
nil |
HUD date format using the tokens DD, MM, YYYY. nil = the language default. |
Access
| Option | Default | Description |
|---|---|---|
Config.Framework |
'auto' |
'auto', 'qbx', 'qb', 'esx' or 'standalone' (case does not matter; the resource names 'qbx_core', 'qb-core' and 'es_extended' work too). An unknown value falls back to auto detection, with a console warning. |
Config.AllowedJobs |
{ police = 0, sheriff = 0 } |
Job name = minimum grade, or true for every grade. false = no access. |
Config.RequireDuty |
true |
Player must be on duty (when the framework reports duty). |
Config.AcePermission |
'helicam.use' |
ACE that always grants access. false = off. On standalone servers it is the only way to get access. |
Config.UseCallsign |
true |
Show the callsign as the HUD unit. false = always Config.UnitFallback. |
Config.UnitFallback |
'AIR-1' |
HUD unit when no callsign is available. |
Config.Agency |
'LSPD AIR SUPPORT' |
Top-left HUD text. Not translated: write it in your language. |
Aircraft
| Option | Default | Description |
|---|---|---|
Config.Helicopters |
{ polmav = true } |
Models with a camera (spawn name, lower case). true = uses Config.Camera.offset. { offset = vec3(x, y, z) } = a mount for this model only (meters: x right, y forward, z up). false = off. |
Config.AllowedSeats |
nil |
nil = every seat. Otherwise a table of seat indices: -1 pilot, 0 front passenger, 1, 2... rear. Example: { [-1] = true, [0] = true }. |
Config.MinAltitude |
5.0 |
Minimum height above ground (m). Below it the camera cannot open and closes itself. |
Config.EnterKey |
{ control = 38, label = 'E', helpToken = 'INPUT_PICKUP' } |
Open / close key: GTA control index, HUD label, and help-text token of the same control. |
Config.HelpText |
true |
Show "Press E to use the helicopter camera" in an allowed seat with access, at or above the minimum height. |
Camera (Config.Camera)
| Option | Default | Description |
|---|---|---|
offset |
vec3(0.0, 2.2, -1.2) |
Default camera mount (polmav nose turret). If the hull is visible, raise y or lower z. |
fovMax |
70.0 |
Widest view (1x). |
fovMin |
3.0 |
Narrowest view (about 23x). |
zoomStep |
0.14 |
Relative FOV change per wheel step. |
zoomLerp |
8.0 |
FOV smoothing speed (1/s). |
rotSpeed |
6.0 |
Mouse sensitivity, scaled down as you zoom in. |
pitchMin |
-89.0 |
Lowest camera angle (degrees). |
pitchMax |
20.0 |
Highest camera angle (degrees). |
rayLength |
2500.0 |
Targeting ray length (m). The server also refuses plate lookups for vehicles farther than rayLength + 200 m from the helicopter. |
maxLockDist |
400.0 |
Max lock distance (m), measured from the camera. Locks break beyond it. Above about 424 m, where OneSync streams entities out, a locked target is followed from server positions. |
tickRate |
50 |
HUD update interval (ms). |
easeMs |
500 |
Camera transition when opening / closing (ms). Keep it above 0: some anticheats flag instant script cameras. |
hideRadar |
true |
Hide the minimap and HUD components while the camera is open. |
Lock (Config.Lock)
| Option | Default | Description |
|---|---|---|
seconds |
3.0 |
Time to acquire a lock, and time to release it. |
Audio surveillance (Config.Audio)
| Option | Default | Description |
|---|---|---|
enabled |
true |
Master switch. |
voiceResource |
'pma-voice' |
Resource name of pma-voice. Change it only if your pma-voice folder has another name. Only pma-voice is supported. |
diameter |
5.0 |
Listening circle diameter (m). The HUD shows the radius (R). |
volume |
0.55 |
Volume on a clear link (0-1). Keep it below radio volume so both stay distinguishable. |
minVolume |
0.28 |
Volume on the worst link. |
updateRate |
500 |
Server range scan interval (ms). |
tickRate |
150 |
Client audio update interval (ms). Dropout chances apply per tick. |
ignoreInVehicle |
true |
Players inside vehicles cannot be heard. |
maxRangeFromHeli |
450.0 |
The server rejects listening points farther than this from the helicopter (m). |
link.clearRange |
150.0 |
Full quality up to this camera-to-point distance (m). |
link.lostRange |
900.0 |
Worst quality from this distance on (m). The default is beyond the default lock limit (400 m): see What the operator hears. |
link.minQuality |
0.12 |
Quality at lostRange (0-1). |
link.curve |
1.2 |
Above 1, quality degrades slowly just after clearRange and faster farther out. |
dropout.chanceClear |
0.0025 |
Chance per tick that a "SIGNAL LOST" dropout starts on a clear link. |
dropout.chanceLost |
0.03 |
The same on the worst link. Set both to 0 for no dropouts. |
dropout.ticksClear |
{ 1, 2 } |
Dropout length in ticks (min, max) on a clear link. |
dropout.ticksLost |
{ 2, 5 } |
The same on the worst link. |
jitter.clear / jitter.lost |
0.04 / 0.22 |
Volume flutter (+/- ratio) on a clear / the worst link. |
occlusion.enabled |
true |
Line-of-sight check. false = walls do not block. |
occlusion.interiorBlocks |
true |
Players inside interiors (MLOs) are never heard. |
occlusion.rayFlags |
81 |
Shape test flags: 1 world + 16 objects + 64 glass. |
occlusion.rayOptions |
6 |
Ignore see-through and non-colliding surfaces. |
occlusion.clearTicks |
2 |
Clear checks in a row before a blocked speaker is heard again. |
occlusion.requireCollision |
true |
Map collision may not be loaded far away. true = treat "unknown" as blocked. |
submixSteps |
{ 0.0, 0.5, 1.0 } |
Number and position of the radio filter steps. 0 = submixClear, 1 = submixLost, and steps in between are blended. |
submixClear |
see config.lua | GTA RadioFx parameters on a clear link (freq_low, freq_hi, fudge, rm_mod_freq, rm_mix, o_freq_lo, o_freq_hi). |
submixLost |
see config.lua | RadioFx parameters on the worst link. |
Searchlight (Config.Spotlight)
| Option | Default | Description |
|---|---|---|
enabled |
true |
false = no searchlight: no key, no LIGHT toggle or hint, and the server ignores light updates. |
key |
{ control = 47, label = 'G' } |
On / off key: GTA control index (number) and HUD label. 47 = INPUT_DETONATE (G). |
diameter |
5.0 |
Lit circle diameter (m). |
maxDistance |
400.0 |
Nothing is lit farther than this from the camera (m). The server accepts points up to 50 m farther from the helicopter. |
color |
{ 255, 244, 229 } |
Light color { r, g, b }, 0-255 (warm white). |
brightness |
30.0 |
Light intensity. |
hardness |
4.0 |
Edge sharpness of the circle. |
falloff |
20.0 |
Falloff of the game's spot light. Check the circle with Config.Debug after changing it. |
shadows |
true |
People, vehicles and objects in the light cast shadows. false saves GPU on every client that draws the light. |
syncMs |
200 |
How often the light position is shared with other players (ms). |
radiusScale |
1.0 |
Calibration of the cone angle: raise it if the lit circle is smaller than diameter, lower it if it is larger. |
A value outside its range falls back to the default with an F8 warning. Ranges: diameter 0.1-200, maxDistance 1-10000, syncMs 50-10000, radiusScale 0.01-10, brightness / hardness / falloff 0-10000.
Plate lookup (Config.Plate)
| Option | Default | Description |
|---|---|---|
enabled |
true |
false = no database lookups. The owner and registration rows show dashes. |
cacheSeconds |
60 |
Cache time per plate (s), on the server and on the client. |
clientHoldMs |
350 |
How long a vehicle must stay under the crosshair before it is queried (ms). |
serverMinIntervalMs |
200 |
Minimum time between two queries from the same player (ms). |
detectFakePlate |
false |
qb-core / qbx_core only: flag unregistered plates that are stored as another vehicle's fake plate ("PLATE MISMATCH"). |
emergencyClassIsFleet |
true |
Emergency-class vehicles (class 18) show as GOVERNMENT VEHICLE. |
Sounds (Config.Sounds)
| Option | Default | Description |
|---|---|---|
enabled |
true |
false mutes every camera sound. |
lock |
{ 'Found_Target', 'POLICE_CHOPPER_CAM_SOUNDS' } |
Lock acquired. Format { soundName, soundSet } (GTA frontend sounds). |
lost |
{ 'Lost_Target', 'POLICE_CHOPPER_CAM_SOUNDS' } |
Lock released or lost. |
thermalOn |
{ 'Thermal_On', 'CAR_STEAL_2_SOUNDSET' } |
Switched to IR. |
thermalOff |
{ 'Thermal_Off', 'CAR_STEAL_2_SOUNDSET' } |
Left IR. |
spot |
{ 'SELECT', 'HUD_FRONTEND_DEFAULT_SOUNDSET' } |
Searchlight switched on or off. |
Logging (Config.Log)
| Option | Default | Description |
|---|---|---|
console |
true |
Activity lines in the server console: camera opened / closed / denied, sessions closed by the server, plate checks, audio locks. Temporary database errors (plate lookup failed, fake plate lookup failed) and the note that a query without optional columns fits your database are printed only with this option too. Startup warnings, webhook errors, errors in your Editable functions and a query fallback that loses owner names are always printed. |
discord |
true |
Discord embeds. They are sent only when a webhook is set. |
Languages
| Code | Language | Code | Language |
|---|---|---|---|
en |
English | nl |
Dutch |
de |
German | it |
Italian |
fr |
French | tr |
Turkish |
es |
Spanish | cs |
Czech |
pt-br |
Portuguese (Brazil) | da |
Danish |
pl |
Polish | sv |
Swedish |
hu |
Hungarian | ro |
Romanian |
Switching languages: set Config.Locale = 'de' and restart the resource.
- Case, spaces and
_do not matter:'pt_BR'and'PT-BR'both selectpt-br. - A code with no file falls back to English, and the server console warns about it.
- A key missing from a translation shows its English text.
What is translated: HUD texts, notifications, the help text, warnings and Discord embed titles and field names.
What stays as is:
- compass letters N / E / S / W and the N / S / E / W letters of the position readout (aviation convention)
Config.AgencyandConfig.UnitFallback(you write them yourself)- console messages (English)
Changing a text: edit the file of your language in locales/, then restart the resource.
Adding a language:
- Copy
locales/en.luatolocales/<code>.lua, e.g.locales/fi.lua. Every file inlocales/loads on its own, so there is nothing to add to the manifest. - Change
Locales['en']toLocales['<code>'], using the same code as the file name. - Translate the texts on the right. Never change the keys on the left.
- Set
Config.Locale = '<code>'.
Rules when translating (also written at the top of locales/en.lua):
- Texts are Lua strings. Write an apostrophe inside
'...'as\'. A syntax error disables the whole language file. - Keep every
%s, because a value replaces it. help_open:- keep
~%s~as it is, because GTA draws the key button there - do not add other
~codes - GTA draws this text with the game font, so check special letters in game
- keep
help_open,err_min_altitude,exit_altitudeandlog_plate_checkgo throughstring.format. Write a literal percent sign there as%%. A translation that breaks this falls back to English, and the console names the key.ui_*texts fill fixed-size HUD panels. Keep them about as long as the English ones.- The monospaced HUD font only has Latin-1 letters. When a text of the monospaced areas (sensor, mode, zoom, FOV, stabilisation, gimbal, point label, units) needs another letter, for example Polish, Czech, Hungarian, Romanian or Turkish letters, the HUD draws those areas with the condensed font instead.
Units and date
Config.Units = {
speed = 'mph', -- 'kmh' | 'mph'
altitude = 'ft', -- 'm' | 'ft'
distance = 'ft', -- 'm' | 'ft'
}
Accepted spellings (case and spaces ignored):
- speed:
kmh,km/h,kph,mph - lengths:
m,meter(s),metre(s),ft,foot,feet
Anything else falls back to km/h or meters, with a console warning.
- speed:
speedsets the ground speed (GS) and the target speed.altitudesets AGL, MSL, the altitude ruler and the minimum-height notifications ("5 m" / "16 ft"). In feet the ruler ticks every 25 ft with labels every 100 ft.distancesets target and person distance, the ground point fallback text and the audio radius (R).Values in
config.luastay in meters and km/h whatever you choose here.
Date: Config.DateFormat = nil uses the language default, e.g. MM/DD/YYYY in English and DD.MM.YYYY in German. Set your own with the tokens DD, MM and YYYY, for example Config.DateFormat = 'YYYY-MM-DD'. The HUD clock shows the player's own computer time in 24 h format.
Discord logging
Create a webhook in your Discord channel settings (Integrations > Webhooks).
Add it to
server.cfg:set xr_helicam_webhook "https://discord.com/api/webhooks/ID/TOKEN"Use
setonly, neversetrorsets.setrsends the value to every player, andsetspublishes it in the server list. Either way, anyone could post to your log channel or delete messages from it.Keep
Config.Log.discord = true.
As an alternative, set Editable.Webhook = 'https://...' in server/editable.lua. That file runs on the server only. The convar wins when both are set. The ready line at start shows discord log: on when Config.Log.discord = true and a valid http(s) URL is configured.
Embeds (their language follows Config.Locale; the sender name is the notify_title text):
| Embed | Contents | Sent |
|---|---|---|
| CAMERA OPENED | Operator (character or player name, server id, citizenid / identifier), unit | At most once per 5 s per player |
PLATE CHECK - <plate> (red when wanted) |
Plate, status (PRIVATE / ORGANIZATION / FLEET VEHICLE, NOT REGISTERED, plus WANTED / PLATE MISMATCH), owner (name + citizenid / identifier, or the society / fleet job), model, operator | Once per operator and plate within Config.Plate.cacheSeconds. Lookups without a vehicle database and without a WANTED flag are not logged. |
| AUDIO SURVEILLANCE LOCK | Location (x, y, z), operator | Once per lock. Locking the same point again (within 1 m) is not repeated. |
- Every embed has a UTC timestamp, which Discord shows in each reader's local time, and the footer
xr-helicam v1.1.0. - Embeds are queued and sent 10 at a time, every 1.5 s. When the queue passes 200 entries, the oldest are dropped.
- The script respects Discord's rate limit (HTTP 429). Other 4xx answers are not retried. If Discord rejects a batch as invalid (HTTP 400), its embeds are sent again one by one.
- The logs contain character names and citizenids / license identifiers, so keep the channel staff-only.
Plate lookup
How it works
- The operator keeps a vehicle under the crosshair for
Config.Plate.clientHoldMs. A locked vehicle is queried at once. - The client sends only the vehicle's network id. The server:
- checks the open camera session
- applies the rate limit
- checks the routing bucket
- checks again that the player is inside the helicopter and still has access
- checks the distance to the helicopter
- reads the plate from the vehicle entity
- The server calls the
server/editable.luahooks in this order:FetchVehicleRecordGetFleetVehicle, if no record was found (or there is no vehicle database)IsFakePlate, on qb/qbx withdetectFakePlate, only for unregistered platesIsVehicleWanted
- The result is cached per plate for
Config.Plate.cacheSeconds, on the server and on the client. A wanted flag or new owner therefore shows up after the cache expires. The owner's citizenid / identifier is never sent to players. It is used only for the Discord log and yourserver/editable.luahooks.
The MODEL row shows the game's own name for the vehicle model. The database model (qb/qbx: shared vehicle list label) goes into the Discord log.
Target panel states
| Panel shows | Meaning |
|---|---|
OWNER GOVERNMENT VEHICLE, REGISTRATION REGISTERED |
Emergency-class vehicle (emergencyClassIsFleet) or Editable.IsFleetVehicle returned true. This overrides the lookup. |
OWNER QUERYING… (typing), REGISTRATION ··· |
Lookup in progress |
OWNER —, REGISTRATION — |
Plate lookup is off (Config.Plate.enabled = false) or there is no vehicle database (standalone, or no query fits your schema) |
OWNER character name, REGISTERED |
Private vehicle |
OWNER UNKNOWN, REGISTERED |
Registered, but no owner name found |
OWNER ORGANIZATION, REGISTERED |
Society / job vehicle (job column, no owner name) |
OWNER FLEET VEHICLE (POLICE), REGISTERED |
Editable.GetFleetVehicle returned a job |
OWNER NO RECORD, UNREGISTERED (red) |
Plate not found. This includes vehicles that exist only on one client (not networked). |
Badge WANTED |
Editable.IsVehicleWanted returned true |
Badge PLATE MISMATCH |
Fake plate detected (qb/qbx, detectFakePlate = true) |
Built-in database queries
All queries search plate IN (?, ?) with two values: the trimmed upper-case plate, and the plate exactly as the game returns it (which may be padded with spaces).
qb-core / qbx_core: player_vehicles pv LEFT JOIN players p ON p.citizenid = pv.citizenid
- Owner name from
players.charinfo(firstname, lastname). Owner job label fromplayers.job. - Some garage scripts store the owning job in extra columns. The first queries also read them:
pv.jobVehicle,pv.job,pv.job_vehicle. A text value such aspolicemarks a society vehicle. Numbers (flags such as1) are ignored. - There are six query steps: from all three job columns down to the stock columns, and finally without the
playersjoin (for example when the two tables use different collations).
ESX Legacy: owned_vehicles ov LEFT JOIN users u ON u.identifier = ov.owner
- Owner name from
users.firstname/users.lastname. Society fromov.job. - There are four query steps: with and without
ov.job, and with and without theusersjoin. owned_vehicleskeeps the model only inside the vehicle properties, so no database model is read.
How steps are chosen: if a step fails with a schema error ("doesn't exist", "unknown column", "unknown table", "illegal mix of collations"), the next step is tried. The step that works is remembered, so schema errors are not repeated. If no step fits, the console prints the reason once, and the HUD shows dashes instead of UNREGISTERED. Other database errors count as temporary: nothing is cached, and the client asks again 2 s later. After 3 failed attempts it waits Config.Plate.cacheSeconds before it tries that plate again.
Standalone: there are no queries, and the HUD shows dashes. GetFleetVehicle and IsVehicleWanted are still asked.
Adapting server/editable.lua
Every function runs on the server, in a thread, in protected mode. MySQL.*.await (oxmysql) calls are fine. A function that throws an error is reported in the console at most once a minute, and its default applies. You can use these helpers:
Bridge.name:'qbx' | 'qb' | 'esx' | 'standalone'Bridge.GetJob(src):{ name, grade, onduty }Bridge.GetIdentity(src):{ id, name }
Editable.FetchVehicleRecord(plate, rawPlate, framework): registration of a plate. Return one of:
| Return | Meaning |
|---|---|
true, record |
Found. record = { owner = 'First Last', ownerId = 'ABC12345', model = 'sultan', ownerJob = 'Police', society = 'mechanic' }. Every field is optional. With owner: private vehicle. Otherwise with society: organization vehicle. Otherwise: private vehicle with an unknown owner. |
true, nil (or return nil) |
Not registered. GetFleetVehicle is asked next. |
false, 'error text' |
Temporary database error. Nothing is cached, and the camera asks again. |
nothing (a bare return) |
No vehicle database. The HUD shows dashes. GetFleetVehicle and IsVehicleWanted are still asked. |
If the function throws an error, it counts as a temporary error.
Example for a custom table (this replaces the built-in queries):
function Editable.FetchVehicleRecord(plate, rawPlate, framework)
local ok, row = pcall(MySQL.single.await,
'SELECT owner_name, model FROM my_vehicles WHERE plate IN (?, ?) LIMIT 1', { plate, rawPlate })
if not ok then return false, row end -- temporary error
if not row then return true, nil end -- not registered
return true, { owner = row.owner_name, model = row.model }
end
Editable.GetFleetVehicle(plate, vehicle): vehicle is the server entity handle. Return { job = 'police' } to show "FLEET VEHICLE (POLICE)", or nil. It is asked only when no record was found or there is no vehicle database. Default on error: nil.
function Editable.GetFleetVehicle(plate, vehicle)
local row = MySQL.single.await('SELECT job FROM police_garage_vehicles WHERE plate = ? LIMIT 1', { plate })
if row then return { job = row.job } end
return nil
end
Do not read replicated state bags such as Entity(vehicle).state.x here. The client that owns a vehicle (usually its driver) can set them.
Editable.IsVehicleWanted(plate, vehicle, info): return true for the WANTED badge. info is read only: { plate, registered, owner, ownerId, model, ownerJob, kind, mismatch }, where kind is 'owned' | 'society' | 'fleet' | 'unregistered' | 'disabled'. It is asked for every looked-up plate (once per plate per cache period). Default on error: false.
function Editable.IsVehicleWanted(plate, vehicle, info)
local row = MySQL.single.await('SELECT 1 AS wanted FROM mdt_vehicles WHERE plate = ? AND wanted = 1 LIMIT 1', { plate })
return row ~= nil
end
Editable.IsFakePlate(plate, rawPlate): qb-core / qbx_core only, and only with Config.Plate.detectFakePlate = true, for plates that are not registered.
- The built-in version checks
player_vehicles.metadata(JSONfakeplate) and afakeplatecolumn. It skips the one your database does not have. - These columns have no index, so every uncached lookup scans the table.
- Default on error:
false.
Turning the lookup off
Set Config.Plate.enabled = false. The client never asks the server, and the owner and registration rows show dashes. Emergency-class vehicles still show as government vehicles.
Audio surveillance
It needs pma-voice running under the name set in Config.Audio.voiceResource, and Config.Audio.enabled = true. Otherwise the audio panel shows AUDIO N/A — NO VOICE LINK and the rest of the camera works normally.
How it works
- Audio starts once a lock on a person or a ground point is complete (TRK LOCK). A vehicle lock shows AUDIO N/A — VEHICLE TARGET.
- The listening circle (
diameter, 5 m) sits on the locked point. For a person, it moves with them and is updated every 500 ms. - The server scans the circle every
updateRatems and sends the operator the list of players inside. The client never chooses who is heard. - The listening point must be within
maxRangeFromHeli(450 m) of the helicopter, otherwise the HUD shows AUDIO OUT OF RANGE. With the defaultConfig.Camera.maxLockDist(400 m) a lock cannot get that far. If the helicopter later moves more than 50 m beyond that limit, the list stays empty until it comes back.
What the operator hears
- Only the player whose camera is open hears the audio. Other crew members hear nothing extra.
- Players inside the circle sound like a radio feed:
- volume from
volume(clear link) down tominVolume(worst link) - a little quieter at the edge of the circle
- flutter (
jitter) - RadioFx distortion (
submixSteps,submixClear,submixLost): the filter moves to a stronger step as the link gets worse
- volume from
- Link quality depends on the distance from the camera to the point: clear up to
link.clearRange(150 m), worst fromlink.lostRange(900 m) on. - With the default settings a listening point never gets that far: a lock breaks beyond
Config.Camera.maxLockDist(400 m), and the server accepts points only up tomaxRangeFromHeli(450 m). At 400 m the link quality is still about 76 % (4 of 5 signal bars). Volume, flutter and dropouts change with distance, but the radio filter stays on its first step (submixClear); the next step starts at about 413 m. For a stronger effect, see Tuning tips. - Random dropouts mute the whole link for a moment ("SIGNAL LOST"). They are rare on a clear link and frequent on a bad one.
- The HUD panel shows:
- signal bars
- a VU meter that reacts to audible speakers
- the number of subjects ("audible/total" when some are behind cover)
- the radius
- a static hiss generated by the HUD that grows as the link gets worse
- Players who are listened to get no notification.
Who is not heard
- Players inside vehicles, when
ignoreInVehicle = true. - Players behind walls, under roofs or inside interiors (occlusion). A line-of-sight ray runs from the camera to the speaker's head every tick. A blocked speaker falls back to normal proximity voice, which means silence from the air. A speaker becomes audible again after
clearTicksclear checks in a row. HUD: NO LINE OF SIGHT. - Speakers whose surrounding map collision is not loaded on the operator's client, when
requireCollision = true. - Players on the same pma-voice radio channel or phone call as the operator. They are already heard over the radio.
- Players flagged as muted (
Player(id).state.muted == true), and players the operator muted in pma-voice. - Players in another routing bucket, and the operator themselves.
Tuning tips
- Clearer audio farther away: raise
link.clearRange/link.lostRangeorlink.minQuality. - Stronger effect within the default lock distance: lower
link.lostRange, for example to400.0. The worst link, the strongest filter step and the most frequent dropouts are then reached at the 400 m lock limit. - No dropouts: set
dropout.chanceClear = 0anddropout.chanceLost = 0. - Voices heard through walls at long range: keep
occlusion.requireCollision = true. Setting it tofalselets speakers through when the collision around them is not loaded yet. - Less distortion: move the
submixLostvalues towardsubmixClear. With the default settings only the first step (submixClear) is heard; to make it cleaner, lower itsfudgeandrm_mix. - Stricter or looser range: lower or raise
maxRangeFromHeli(the server enforces it) andConfig.Camera.maxLockDist(the client lock limit).
Searchlight
While the camera is open, G switches the searchlight on and off (Config.Spotlight, on by default). The HUD's LIGHT toggle next to NV / IR lights up while it is on, and the key hints end in G LIGHT. Every camera opens with the light off, and the light goes off when the camera closes for any reason or the resource stops.
- Where it shines: on the vehicle, person or ground point under the crosshair. While a lock is held, it follows the locked target, also after the target streams out of the operator's range. The sky, or a point farther than
maxDistance(400 m) from the camera, lights nothing: the light stays on and shines again once the camera looks at a point in range. - What others see: the operator's client draws its own light without network delay. All other players, crew and players on the ground, see the same circle, shining from the helicopter's direction. The point is shared at most every
syncMs(200 ms), only when it moved more than 0.5 m, and other clients move the light smoothly between updates. A client draws up to 8 lights of other players at once (the nearest), only for players within its OneSync range. The server checks every point: open camera, operator inside the helicopter, point withinmaxDistance+ 50 m of it. - Constant circle: the light starts 30 m before the lit point (at the camera when it is closer) and its cone covers
diameterat that distance, so the circle keeps its size at any range and zoom. At a shallow angle it stretches into an oval, like a real searchlight. - Calibration: the visible edge also depends on
brightness,hardness,falloffand the ambient light. SetConfig.Debug = trueand look straight down at flat ground: the operator sees a thin white ring of exactlydiameteron the lit point. RaiseradiusScale(e.g.1.2) if the lit circle is smaller than the ring, lower it if it is larger. SetConfig.Debug = falseagain afterwards. - Performance: with
shadows = true, people, vehicles and objects in the light cast shadows, which costs some GPU on every client that draws it. Useshadows = falsefor weaker PCs. Each client looks for other players' lights every 500 ms. Per-frame drawing runs only while a light is on nearby. - The light is not logged to the console or Discord.
Integration API
Exports
-- client: true while the camera is open
local open = exports['xr-helicam']:IsActive()
-- client: true while the searchlight is switched on (also while it lights nothing, e.g. the sky)
local lit = exports['xr-helicam']:IsSpotlightOn()
-- server: true while that player's camera is open
local using = exports['xr-helicam']:IsUsingCamera(source)
Local client events
These fire on the operator's client only, with TriggerEvent (not a net event):
AddEventHandler('xr-helicam:opened', function(heli) -- heli = helicopter entity
-- camera opened
end)
AddEventHandler('xr-helicam:closed', function()
-- camera closed (sent only after a matching 'opened')
end)
State bag
LocalPlayer.state.helicam is true while the camera is open, and false or nil otherwise. It is not replicated: only the operator's own client can read it. Server scripts use IsUsingCamera.
Player(serverId).state.helicamSpot is replicated (server and every client): { x, y, z, hx, hy, hz } while that player's searchlight lights a point (the lit point and the helicopter position, rounded to 0.1 m), nil otherwise. Only the xr-helicam server writes it. A modified client can write its own player state, so use it for display only, never to grant anything.
Key conflicts (dispatch, radial menus, ...)
The camera uses E, ESC and G (searchlight). While it is open, it disables the GTA controls it uses (see Controls). Other scripts still react to these keys if they use a key mapping (RegisterKeyMapping commands, for example "accept dispatch call" on E or an emote or radial menu on G) or read another control on the same key (for example the helicopter spotlight of qb-policejob, control 183 on G). Skip them there:
-- in the other script's key handler (client)
if LocalPlayer.state.helicam then return end
This line is safe even when xr-helicam is not installed, because the value is then nil.
A script that opens its own helicopter camera on E (for example qb-policejob or qbx_policejob) needs more than this line. See Conflicts with other scripts.
Client hooks (client/editable.lua)
| Function | Default | Use |
|---|---|---|
Editable.Notify(message, nType) |
ox_lib lib.notify, title notify_title, 5000 ms |
Route notifications to your own notify resource. nType: 'error', 'warning', 'success' or 'info'. |
Editable.CanOpenCamera(vehicle) |
true |
Extra client checks (handcuffed, dead, ...). Return false to block, and show your own message. The server still checks everything. |
Editable.OnCameraOpened(vehicle) |
nothing | Hide your HUD, pause other scripts. |
Editable.OnCameraClosed() |
nothing | Undo OnCameraOpened. It also runs when the resource stops. |
Editable.IsFleetVehicle(vehicle) |
emergency class (18) when emergencyClassIsFleet |
true = the panel shows GOVERNMENT VEHICLE. It runs every frame for the vehicle under the crosshair, so keep it cheap. |
Every hook runs in protected mode. A failing hook prints a warning in F8 at most once a minute, and the camera keeps working.
Server hooks (server/editable.lua)
| Function | Default | Use |
|---|---|---|
Editable.Webhook |
'' |
Discord webhook (the convar wins). |
Editable.CanUseCamera(src, job) |
true |
Extra access rule after the job / grade / duty checks. ACE players skip it. It runs often (key pre-check, opening, plate / audio checks), so keep it cheap, with no database queries. job = { name, grade, onduty }. On error: allowed. |
Editable.GetCallsign(src) |
nil |
HUD unit name. nil = framework callsign, then Config.UnitFallback. On error: nil. |
Editable.FetchVehicleRecord / GetFleetVehicle / IsVehicleWanted / IsFakePlate |
see Plate lookup |
Do not grant access from replicated state bags such as Player(src).state.x, because a player's client can set them. Use a server export or a server-side table instead.
The net events of the resource (xr-helicam:server:* / xr-helicam:client:*) are internal. Do not trigger them from other scripts.
Open and encrypted files
The resource is delivered through Cfx.re Asset Escrow.
| Open (you can edit them) | Encrypted |
|---|---|
fxmanifest.lua |
client/main.lua |
config.lua |
client/audio.lua |
locales/*.lua |
client/spotlight.lua |
client/editable.lua |
server/main.lua |
server/editable.lua |
bridge/server.lua |
html/index.html, html/app.js, html/style.css, html/img/noise.png (the HUD; escrow never encrypts NUI files) |
shared/locale.lua |
The escrow_ignore list in fxmanifest.lua is config.lua, locales/*.lua, client/editable.lua and server/editable.lua. Leave fxmanifest.lua unchanged.
Troubleshooting / FAQ
The resource does not start
FXServer prints one of these lines and skips xr-helicam:
| Console line | Fix |
|---|---|
Could not find dependency /onesync for resource xr-helicam |
OneSync is off. Enable it in the txAdmin FXServer settings, or start FXServer with +set onesync on. |
Could not find dependency ox_lib for resource xr-helicam |
Install ox_lib (folder name ox_lib) and ensure ox_lib before xr-helicam. |
Could not find dependency oxmysql for resource xr-helicam |
Install oxmysql (folder name oxmysql) and ensure oxmysql before xr-helicam. Standalone servers need it too, but no database. |
No ready line, and errors that end in attempt to index a nil value (global 'Config'). config.lua did not load, usually because of a syntax error after an edit. The other files cannot run without it. The server console shows this error for server/main.lua, bridge/server.lua and shared/locale.lua, the F8 console of every player shows it for client/main.lua, and the camera does not open. Those files are encrypted, but the error is not in them. Scroll up to the first xr-helicam error: it names config.lua and the line to fix, for example @xr-helicam/config.lua:107: '}' expected (to close '{' at line 105) near 'fovMax' (a missing comma before fovMax). Fix it, then restart the resource.
The camera does not open
No help text, and E does nothing. The client only listens for the key when all of these are true:
- The helicopter model is in
Config.Helicopters. Use the spawn name in lower case. For add-on helicopters, use the name the vehicle spawns with. - The seat is allowed (
Config.AllowedSeats). - The player has access (ACE, or job + grade + duty +
Editable.CanUseCamera). Without access there is no message at all. - No NUI has focus.
The help text also needs Config.HelpText = true and a height of at least Config.MinAltitude. Below that height, E still answers with the altitude notification. After job or ACE changes, re-enter the helicopter or wait up to 15 s. If the server console shows no ready line, see The resource does not start.
Find the failing check with Config.Debug. Set Config.Debug = true in config.lua, restart the resource and sit in an allowed seat of a configured helicopter. The server console prints one line per player, and prints it again only when the answer changes:
[xr-helicam] debug: John Doe (3) has camera access (framework qb, job police, grade 2, on duty true, ACE helicam.use not granted)
[xr-helicam] debug: Jane Roe (7) has no camera access: err_no_access, grade 0 is below the minimum grade 2 of Config.AllowedJobs['sheriff'] (framework qbx, job sheriff, grade 0, on duty true, ACE helicam.use not granted)
The reason names the check that failed: the job is not in Config.AllowedJobs (the line also points out an entry that differs only in upper / lower case), the grade is below the minimum, the player is off duty, the server runs standalone without the ACE, Editable.CanUseCamera returned false, or the framework returned no player (character not loaded yet, or the wrong framework: see Config.Framework). If the line says has camera access, check condition 4 and the F8 console. If no debug: line appears at all, the client never asked the server: check the model and the seat (conditions 1 and 2) and look for Config.Helicopters / Config.AllowedSeats warnings at startup (see Console messages). Set Config.Debug = false again when you are done.
A notification appears:
| Notification | Cause / fix |
|---|---|
| The helicopter must be at least 5 m above the ground... | Climb above Config.MinAltitude. |
| This helicopter is not networked. | The helicopter was spawned on one client only. Spawn it as a networked vehicle. |
| You are not authorized to use this camera. | Job not in Config.AllowedJobs, grade too low, standalone without the ACE, or Editable.CanUseCamera returned false. |
| You must be on duty to use the camera. | Config.RequireDuty = true and the player is off duty. |
| Player data not found. | The framework has no player for this id (character not loaded), or the wrong framework is in use. Check the framework in the ready line, start xr-helicam after the framework, or set Config.Framework. |
| This aircraft has no camera. | The server could not find that helicopter (for example it was just deleted) or its model in Config.Helicopters. Check the spawn name and restart the resource after config changes. |
| You are not inside the helicopter. / The camera cannot be used from this seat. | The server sees the player outside that helicopter, or in a seat that Config.AllowedSeats does not allow. |
| Too many attempts. Please wait. | The camera was opened or tried again within 0.5 s of the last attempt. |
| The server did not respond. Try again. | The server callback timed out. Check the server console for errors. |
| The camera cannot be used right now. | A script error. The F8 console shows error while opening the camera (check recent config.lua changes) or error while running the camera (...) with details. |
The three access messages (not authorized, on duty, player data) are rare: a player without access gets no key and no message at all (see above). They show only when access was lost after the client last asked the server (see When access changes).
The camera closes right away. It closes below Config.MinAltitude, when the pause menu opens, when the player leaves the helicopter or dies, and when the server closes the session (with Config.Log.console = true the server console shows session closed by the server: <reason>).
The helicopter hull is visible in the camera. Adjust Config.Camera.offset, or give the model its own { offset = vec3(...) } in Config.Helicopters.
Two cameras open at once, or another camera overlay stays on screen. Another script opens its own helicopter camera on the same key. On qb-core / qbx_core this is usually the one built into qb-policejob or qbx_policejob (E in the polmav). Turn it off as described in Conflicts with other scripts.
No owner is shown
- Dashes (—): plate lookup is off, the server runs standalone, or no query fits your database. In the last case the console printed
... lookup disabled: no query matches your database: adaptEditable.FetchVehicleRecord. Dashes also appear whenserver/editable.luadid not load (console:server/editable.lua did not load). - UNKNOWN: the vehicle is registered, but its owner has no character row (for example a deleted character), or the query step without the owner join is in use. In the second case the console printed
... lookup: using query N of M, owner names are not shown because the ... table cannot be joined [...]: see Console messages. - NO RECORD / UNREGISTERED: the plate is not in the database. This covers NPC cars, vehicles spawned by admins or job garages, and cars with a changed plate. Job garage vehicles can be mapped with
Editable.GetFleetVehicle. - GOVERNMENT VEHICLE on every police car: that is
emergencyClassIsFleet = true. - QUERYING… never ends: the server returned no answer. Check the console for
plate lookup failed(database error, printed withConfig.Log.console = true). Also note that vehicles in another routing bucket or farther thanrayLength + 200m from the helicopter are not looked up. - A change does not show: results are cached for
Config.Plate.cacheSeconds(60 s).
No audio
- AUDIO N/A — NO VOICE LINK:
Config.Audio.enabled = false, or pma-voice is not started under the name inConfig.Audio.voiceResource. - AUDIO N/A — VEHICLE TARGET: you locked a vehicle. Lock the person or a ground point instead. Occupants of vehicles are not heard with
ignoreInVehicle = true. - SUBJECTS 0: nobody is within the circle (radius =
diameter / 2). Also check vehicles, the same radio channel or call, and routing buckets. - NO LINE OF SIGHT, or subjects shown as
0/2: the speakers are behind walls or roofs, or inside an interior. Change your angle. At long range, the collision around them may not be loaded (requireCollision). - AUDIO OUT OF RANGE: the point is farther than
maxRangeFromHelifrom the helicopter. - Audio does not start while ACQ is shown: it starts only when the lock completes (TRK LOCK).
- Nothing at all: make sure you did not mute that player in pma-voice. Check the F8 console too.
The HUD text is in English
Config.Localehas no matching file. The server console saysConfig.Locale "xx": locales/xx.lua is missing, did not load (syntax error? see the Lua error above) or does not define Locales['xx'], using English.- The language file has a syntax error, so FiveM skipped it. The same console line appears, and the FiveM console shows the Lua error for that file.
- The
Locales['xx']name inside the file does not match the file name. - Single texts are missing from the translation (they fall back to English), or a
%s/%%rule was broken (the server console or F8 names the key). - The resource was not restarted after the change.
Other questions
Can the pilot use the camera? Yes, with Config.AllowedSeats = nil (every seat). For the co-pilot only, use { [0] = true }. Each allowed crew member can open their own camera at the same time.
Other voice resources (SaltyChat, TokoVOIP, mumble-voip...)? Audio surveillance supports pma-voice only. Everything else works without it.
Anticheat flags the camera? The camera:
- uses a script camera (
CreateCam/RenderScriptCams, eased byeaseMs) - uses
SetNightvision/SetSeethrough - uses the Mumble natives for audio (
MumbleAddVoiceChannelListen,MumbleSetVolumeOverrideByServerId,MumbleSetSubmixForServerId)
It does not modify the player ped. Whitelist xr-helicam for these checks if your anticheat flags them. If the webhook stays silent, your firewall or anticheat may be blocking outgoing HTTP requests (console: webhook did not respond within 20 s).
Performance?
- Outside camera helicopters, the client checks the player's vehicle every 750 ms.
- Per-frame work runs only while the player sits in a configured helicopter with access, or while the camera is open.
- Server loops only iterate over open cameras and active audio locks.
- Every client looks for searchlights of other players every 500 ms and draws them only while one is on nearby. Searchlight shadows cost some GPU (
Config.Spotlight.shadows).
Fonts? The HUD loads two fonts from Google Fonts. If that is blocked, local fallback fonts are used.
The escrow says I do not own the resource. Your server's license key must belong to the Cfx.re account that received xr-helicam. Generate the key on that account at portal.cfx.re.
Console messages
Lines start with [xr-helicam]. Server console, unless marked F8. Lines marked (log) appear only with Config.Log.console = true, lines marked (debug) only with Config.Debug = true. FXServer's own Could not find dependency ... for resource xr-helicam lines, and the errors that follow a broken config.lua, are explained in The resource does not start.
| Message | Meaning / fix |
|---|---|
v1.1.0 ready | framework: X | helicopter models: N | discord log: on/off |
Normal start. Check that the framework is the one you expect. |
Config.Helicopters is empty: no aircraft has a camera |
Add at least one model. |
Config.Helicopters[...] = ... is ignored: write ... = true / Config.Helicopters must be a table such as { polmav = true }... / Config.Helicopters has no usable entry (spawn_name = true): no aircraft has a camera |
Wrong format, or every model is set to false. Write spawn name = true (or { offset = vec3(...) }), for example polmav = true. A list such as { 'polmav' } matches no model. |
Config.AllowedJobs[...] = ... is ignored: write police = 0 (minimum grade) or police = true |
Wrong entry format (see Permissions). |
Config.AllowedJobs must be a table such as { police = 0 }... |
AllowedJobs is not a table. No job has access. |
Config.AllowedSeats must be nil (every seat) or a table such as { [-1] = true, [0] = true }... / Config.AllowedSeats allows no seat: nobody can use the camera... / Config.AllowedSeats: [1] = -1, [2] = 0 match no seat, write { [-1] = true, [0] = true }... |
Wrong seat format. The seats it does not allow get no help text, and the key does nothing there, without a message. Use nil (every seat) or seat index = true, for example { [-1] = true, [0] = true }. A list such as { -1, 0 } matches no seat; the last line suggests the corrected table. |
standalone mode with Config.AcePermission disabled: nobody can use the camera |
Set an ACE, or install / select a framework. |
unknown Config.Framework "x", detecting automatically |
Use 'auto', 'qbx', 'qb', 'esx' or 'standalone'. |
framework "qb" is selected but qb-core is not running |
The forced or detected framework resource is not started. Fix the start order. |
Config.Locale "xx": locales/xx.lua is missing, did not load (syntax error? ...) or does not define Locales['xx'], using English |
Missing file, syntax error in it (the Lua error is printed above), or wrong Locales['xx'] name. See Languages. |
locales/en.lua did not load (syntax error?)... |
Restore the original locales/en.lua. |
Server or F8: Locales['xx'].key does not fit its values (keep every %s...) |
A translation broke a %s / %% rule. The English text is shown until you fix it. |
Config.Units... is not supported, using kmh / using m |
Unknown unit spelling. |
Config.Units must be a table such as { speed = 'mph', altitude = 'ft', distance = 'ft' }: using metric |
Config.Units is not a table. |
server/editable.lua did not load (syntax error?)... |
Fix the syntax error: plate lookup, Editable.Webhook and your other changes are off until then. |
Editable.FetchVehicleRecord is missing from server/editable.lua... |
Registration lookup is off. Restore the function. |
server/editable.lua Editable.X failed: ... |
Your hook threw an error. Its default is used, and the error repeats at most once a minute. |
player_vehicles lookup disabled: no query matches your database (...) / owned_vehicles ... |
None of the built-in queries fits your schema. Adapt Editable.FetchVehicleRecord. |
(log) player_vehicles lookup: using query N of 6, optional garage job column(s) ... found / ..., the standard columns (...) / owned_vehicles lookup: using query 2 of 4, no owned_vehicles.job column (...) |
Information: your database lacks some optional columns, which is normal. With Config.Debug = true the database message is added in brackets. |
player_vehicles lookup: using query 6 of 6, owner names are not shown because the players table cannot be joined [...] / owned_vehicles lookup: using query 3 of 4 or 4 of 4, owner names are not shown because the users table cannot be joined... [...] |
Always printed. Owner names are off: player-owned vehicles show UNKNOWN. Fix the database error in brackets (usually player_vehicles.citizenid and players.citizenid, or owned_vehicles.owner and users.identifier, use different collations) and restart the resource, or adapt Editable.FetchVehicleRecord. |
(log) plate lookup failed (PLATE): ... |
Temporary database error. The client retries. |
(log) fake plate lookup failed: ... |
Database error in the fake plate check. |
the Discord webhook is not an http(s) URL and is ignored |
Check the convar / Editable.Webhook value. |
webhook did not respond within 20 s (blocked by firewall or anticheat?)... |
Outgoing HTTP to Discord is blocked. |
webhook rejected the request (HTTP 401/404...) |
The webhook was deleted or the URL is wrong. |
webhook rejected a batch of N embeds (HTTP 400), sending them one by one |
One embed was invalid. The rest are resent one at a time. |
webhook error X, gave up after 5 attempts... |
Discord or the network failed repeatedly. |
(log) Name (id) camera denied: err_... |
The reason key matches the notification in The camera does not open. |
(debug) debug: Name (id) has camera access (...) / debug: Name (id) has no camera access: err_..., <reason> (framework ..., job ..., grade ..., on duty ..., ACE ...) |
The key pre-check for a player in an allowed seat of a camera helicopter, printed again only when it changes. The reason names the check that failed: see The camera does not open. |
(log) ... session closed by the server: exit_left_heli / exit_heli_missing / exit_access_lost / exit_char_changed |
The server closed a camera: left the helicopter, helicopter gone, access lost, character switched. |
(log) ... listening point rejected: N m from the helicopter (limit N m) |
Audio point beyond maxRangeFromHeli. |
(log) ... listening point out of range of the helicopter, speakers withheld |
The helicopter moved more than 50 m beyond maxRangeFromHeli from an active listening point. Nobody is heard until it comes back. |
(debug) debug: Name (id) searchlight point rejected: N m from the helicopter (limit N m) / debug: Name (id) searchlight update with invalid data ignored |
The server showed no light for this update: the point is farther than Config.Spotlight.maxDistance + 50 m from the helicopter, or the data was invalid. Printed again only when the reason changes. |
framework event handler failed: ... |
Error inside a job / logout handler. Send the full line to support. |
(debug) Bridge.GetJob failed: ... and similar |
A framework call threw an error and counts as "no data" (for GetJob: no player, so no access). Check that Config.Framework matches your server and that xr-helicam starts after the framework, otherwise send the full line to support. |
F8: client/editable.lua did not load (syntax error?): the built-in defaults are used |
Fix the syntax error in client/editable.lua. |
F8: client/editable.lua: Editable.X failed: ... |
Your client hook threw an error. |
F8: Config.EnterKey.control must be a control index number such as 311 (K), not '...': using 38 (E) / Config.EnterKey must be a table... / Config.EnterKey.helpToken must be the control's INPUT_* name... |
Wrong key format: write the control index as a number and helpToken as the same control's INPUT_* name, e.g. { control = 311, label = 'K', helpToken = 'INPUT_REPLAY_SHOWHOTKEY' } (not 47 / G, the searchlight's default key). |
F8: Config.Camera.offset must be vec3(x, y, z)... / Config.Helicopters["x"].offset must be vec3(x, y, z)... |
Wrong offset format. A default is used. |
F8: Config.Spotlight.X must be a number from A to B, not ...: using D / Config.Spotlight.color must be { r, g, b }... / Config.Spotlight.key must be a table... / Config.Spotlight.key.control must be a control index number such as 47 (G)... / Config.Spotlight.key.label is missing... |
Wrong searchlight setting. The default is used (see Searchlight for the ranges). |
F8: Config.Spotlight.key.control N also closes the camera: the searchlight key is disabled... |
The searchlight key equals Config.EnterKey.control, ESC (200) or Backspace (202). Pick another key. |
F8: Config.Spotlight is missing from config.lua (older config?)... / Config.Spotlight must be a table... / client/spotlight.lua did not load... |
The searchlight is off. Copy the Config.Spotlight block from the new config.lua, or replace the whole resource folder (see Updating). |
F8: searchlight error while drawing: ... / searchlight error while reading the lights of other players: ... |
A Lua error, printed once. Send the full line to support. |
(debug) F8: searchlight of player N: on / off |
The light of another player appeared or disappeared on this client. |
F8: error while opening the camera / error while running the camera (check recent config.lua changes) |
A Lua error. The camera closes safely. Check recent config changes, otherwise send the full line to support. |
Updating
- Download the new version from portal.cfx.re (Granted Assets) and read
CHANGELOG.md(its notes are also in the Changelog section below). - Back up your
config.lua, your changedlocales/*.lua,client/editable.luaandserver/editable.luaoutside the resources folder. - Replace the whole
xr-helicamfolder. Never mix files of two versions: the encrypted files belong together. - Re-apply your changes. Compare them with the new files rather than overwriting them, because new versions can add options or texts. Texts missing from an older language file fall back to English.
- Restart the server and check the ready line.
Support
Contact the seller through the support channel named on the store page where you bought xr-helicam. Include:
- the ready line from the server console (version and framework)
- every
[xr-helicam]andSCRIPT ERRORline from the server console and the F8 console, in full - your framework and pma-voice versions
- what you changed in
config.lua,locales/*.luaand the editable files
Most errors after a change come from an open file. Check Troubleshooting / FAQ and Console messages first.
License
See LICENSE.txt. The license covers use on servers of the purchasing Cfx.re account. Redistribution of any part is not allowed, including the open files and the HUD.
Changelog
1.1.0 - searchlight
- Added a searchlight:
- G (configurable,
Config.Spotlight) lights the point under the crosshair, or follows the locked target - lit circle of constant size (default 5 m across, up to 400 m from the camera), optional shadows
- synced through the server, so every player nearby sees it; LIGHT toggle and key hint on the HUD
- calibration ring with
Config.Debug - client export
IsSpotlightOn, replicated player statehelicamSpot
- G (configurable,
- The searchlight key is refused (with a warning) when it equals the open / close key, ESC or Backspace.
- Documentation: key examples now use K (311) instead of G, which is the searchlight's default key.
1.0.0 - initial release
- Helicopter camera for configured models:
- seat restriction and minimum height above ground
- optional per-model camera mount
- optional help text
- configurable open / close key
- Sensor HUD:
- EO, LLTV night vision and IR white-hot thermal modes
- smooth zoom from 1x to about 23x
- compass, altitude ruler (AGL / MSL), position, ground speed, clock and warning banner
- no NUI focus
- Target lock on vehicles, persons and ground points:
- timed acquire and release
- automatic tracking
- OneSync server-side position updates, so a lock distance raised beyond the streaming range (about 424 m) keeps tracking
- Server-side plate lookup:
- qb-core / qbx_core
player_vehiclesand ESX Legacyowned_vehicles, with schema fallback queries - private / organization / fleet / unregistered states, WANTED and PLATE MISMATCH badges
- per-plate cache and rate limits
- qb-core / qbx_core
- Optional audio surveillance through pma-voice:
- listening circle 5 m across
- radio-style sound; volume, flutter and dropouts follow the distance; configurable distortion steps
- line-of-sight occlusion
- server-side range scan
- Access:
- qbx_core, qb-core and ESX Legacy (job, grade, duty)
- standalone mode with ACE permission
- access checked again while the camera is open
Config.Debug: the server console names the check that denies a player access
- Open files:
config.lualocales/*.luaclient/editable.luaandserver/editable.lua(notifications, extra checks, HUD hooks, callsigns, vehicle records, fleet, wanted and fake plate hooks)- the HUD in
html/(NUI files are never encrypted)
- Integration:
- exports
IsActive(client),IsUsingCamera(server) - local events
xr-helicam:opened/xr-helicam:closed - state bag
helicam
- exports
- 14 languages; km/h or mph, meters or feet; configurable date format.
- Console log and Discord webhook log (rate limited, batched, 429 aware).
