-- Variables -- local inHeliCam = false local helicopter = {} local movementInput = false local isMarkersThreadActive = false local lastRappelKeyPress = 0 local pauseMenu = false local visionState = 0 local currentFov = 0.0 local fov = Config.Camera.Zoom.Max local tabletObj = nil local cameraAction = false local instScaleform = nil local submix = false local postalsActive = Config.ShowPostalCodes local spotlights = {} local Units = {} -- Unit conversions, gets dynamically created. local markers = {} local postals = {} local displayPostals = {} local markerBlips = {} local blipStateBagHandlers = {} -- 16 = INPUT_SELECT_NEXT_WEAPON, 17 = INPUT_SELECT_PREV_WEAPON, 75 = INPUT_VEH_EXIT, 80 = INPUT_VEH_CIN_CAM, -- 81 = INPUT_VEH_NEXT_RADIO, 82 = INPUT_VEH_PREV_RADIO, 85 = INPUT_VEH_RADIO_WHEEL, 99 = INPUT_VEH_SELECT_NEXT_WEAPON local controlActions = { 16, 17, 75, 80, 81, 82, 85, 99 } local sounds = { enter = -1, exit = -1, turn = -1, zoom = -1, bleep = -1, scanLoop = -1, scanSuccess = -1, scanFailure = -1, rappel = -1, thermal = -1, spotlight = -1 } local targetBlip = { display = false, handler = nil } local cache = { helicopter = {}, target = {}, camera = {} } local spotlight = { active = false, isThreadActive = false, cameraLockThread = false, brightness = Config.Spotlight.DefaultBrightness, adjustingBrightness = false, radius = Config.Spotlight.DefaultRadius, adjustingRadius = false } local cameraLock = { active = false, attempting = false, type = nil, prevType = nil, entity = nil, coords = nil, timeout = 0, progress = 0 } local camera = { cam = nil, pitch = 0, heading = 0, bearing = 0 } -- Utils -- local function DisplayNotification(msg) BeginTextCommandThefeedPost("STRING") AddTextComponentSubstringPlayerName(msg) EndTextCommandThefeedPostTicker(false, false) -- Comment out above and add custom notification below: --exports.mythic_notify:SendAlert('error', msg) end local function DoesHelicopterHaveCamera(model, vehicle) if Config.Helicopters[model] then return true elseif Config.CanUseAnyHelicopter then local class = GetVehicleClass(vehicle) if class == 15 or class == 16 then return true end end return false end local function GetHeliCameraOffset(model) return (Config.Helicopters[model] and Config.Helicopters[model].offset) or Config.Helicopters.default.offset end local function CanPlayerUseCameraFromCurrentSeat(playerPed, heli, model) local policy = Config.PassengerOnly if Config.Helicopters[model] and Config.Helicopters[model].passengerOnly ~= nil then policy = Config.Helicopters[model].passengerOnly end if policy then -- Checks if we are the pilot of the helicopter if GetPedInVehicleSeat(heli, -1) == playerPed then return false, 'IsPilot' end -- If only rear passangers, then check if we are in the front passanger seat if policy == 2 and GetPedInVehicleSeat(heli, 0) == playerPed then return false, 'NotInRear' end end return true end local function SetHelicopterStateBag(bagName, value) -- TODO: move this to LocalPlayer state like spotlight? if NetworkGetEntityOwner(helicopter.entity) == PlayerId() then Entity(helicopter.entity).state:set(bagName, value, true) else TriggerServerEvent('helicam:setStateBag', helicopter.netId, bagName, value) end end local function SynchroniseSpotlight(data) LocalPlayer.state:set('heliCamSpotlightData', data, true) end local function GetCartesianCoords(coord) local degrees = math.floor(coord) local min = (coord - degrees) * 60 local minutes = math.floor(min) local sec = (min - minutes) * 60 local secfloor = math.floor(sec) local seconds = string.format("%02d", secfloor)..string.format("%.2f", sec - secfloor):sub(2) local cartesian = string.format("%02d", degrees).."° "..string.format("%02d", minutes).."' "..seconds..'"' return cartesian end local function GetHeadingBetweenCoords(from, to) local dx = to.x - from.x local dy = to.y - from.y local heading = GetHeadingFromVector_2d(dx, dy) return heading end local function RotationToHeading(rotation) local heading = rotation if heading < 0 then heading = heading*-1 heading = heading + math.abs(heading - 180.0)*2 end heading = (heading - 360) *-1 return heading end local function RotAnglesToVec(rot) local z = math.rad(rot.z) local x = math.rad(rot.x) local num = math.abs(math.cos(x)) return vector3(-math.sin(z)*num, math.cos(z)*num, math.sin(x)) end local function IsTableEmpty(table) for _ in pairs(table) do return false end return true end local function ShouldLockOntoCenter(entityType) if entityType == 1 and Config.LockOntoCenter.Peds then return true elseif entityType == 2 and Config.LockOntoCenter.Vehicles then return true end return false end local function EnableSubmix() SetAudioSubmixEffectRadioFx(0, 0) SetAudioSubmixEffectParamInt(0, 0, `default`, 1) SetAudioSubmixEffectParamFloat(0, 0, `freq_low`, 625.0) SetAudioSubmixEffectParamFloat(0, 0, `freq_hi`, 8000.0) SetAudioSubmixEffectParamFloat(0, 0, `fudge`, 0.5) SetAudioSubmixEffectParamFloat(0, 0, `rm_mix`, 50.0) submix = true end local function DisableSubmix() SetAudioSubmixEffectRadioFx(0, 0) SetAudioSubmixEffectParamInt(0, 0, `enabled`, 0) submix = false end local function DrawText3D(coords, text) --Style the text SetTextColour(255, 255, 255, 255) SetTextScale(0.0, 0.35) SetTextFont(4) SetTextOutline() -- Diplay the text BeginTextCommandDisplayText("STRING") AddTextComponentSubstringPlayerName(text) SetDrawOrigin(coords.x, coords.y, coords.z, 0) EndTextCommandDisplayText(0.0, 0.0) ClearDrawOrigin() DrawRect(coords.x, coords.y, 1.0, 1.0, 230, 230, 230, 255) end local function GetOffsetFromCoordsInWorldCoords(position, rotation, offset) local rotX, rotY, rotZ = math.rad(rotation.x), math.rad(rotation.y), math.rad(rotation.z) local matrix = {} matrix[1] = {} matrix[1][1] = math.cos(rotZ) * math.cos(rotY) - math.sin(rotZ) * math.sin(rotX) * math.sin(rotY) matrix[1][2] = math.cos(rotY) * math.sin(rotZ) + math.cos(rotZ) * math.sin(rotX) * math.sin(rotY) matrix[1][3] = -math.cos(rotX) * math.sin(rotY) matrix[1][4] = 1 matrix[2] = {} matrix[2][1] = -math.cos(rotX) * math.sin(rotZ) matrix[2][2] = math.cos(rotZ) * math.cos(rotX) matrix[2][3] = math.sin(rotX) matrix[2][4] = 1 matrix[3] = {} matrix[3][1] = math.cos(rotZ) * math.sin(rotY) + math.cos(rotY) * math.sin(rotZ) * math.sin(rotX) matrix[3][2] = math.sin(rotZ) * math.sin(rotY) - math.cos(rotZ) * math.cos(rotY) * math.sin(rotX) matrix[3][3] = math.cos(rotX) * math.cos(rotY) matrix[3][4] = 1 matrix[4] = {} matrix[4][1], matrix[4][2], matrix[4][3] = position.x, position.y, position.z matrix[4][4] = 1 local x = offset.x * matrix[1][1] + offset.y * matrix[2][1] + offset.z * matrix[3][1] + matrix[4][1] local y = offset.x * matrix[1][2] + offset.y * matrix[2][2] + offset.z * matrix[3][2] + matrix[4][2] local z = offset.x * matrix[1][3] + offset.y * matrix[2][3] + offset.z * matrix[3][3] + matrix[4][3] return vector3(x, y, z) end -- General Functions -- local function CanPlayerUseCamera(playerPed) -- Check for jobs local whitelisted, jobMessage = JobCheck() if not whitelisted then return false, jobMessage end -- If we aren't in any vehicle or we can't use this heli if not DoesHelicopterHaveCamera(helicopter.model, helicopter.entity) then if IsPedInAnyPlane(playerPed) then return false, 'NoCameraPlane' else return false, 'NoCameraHeli' end end -- Check seat local canUseFromCurrentSeat, seatMessage = CanPlayerUseCameraFromCurrentSeat(playerPed, helicopter.entity, helicopter.model) if not canUseFromCurrentSeat then return false, seatMessage end -- Check if the camera for this helicopter already is in use local heliEntity = Entity(helicopter.entity) if heliEntity and heliEntity.state.heliCamInUse then return false, 'CameraInUse' end -- Check if someone already use camera functions such as spotlight and camera lock if spotlights[helicopter.netId] then return false, 'SpotlightInUse' end return true, nil end local function Raycast(startCoords, destination, entity, flag) local rayHandle = StartShapeTestLosProbe(startCoords.x, startCoords.y, startCoords.z, destination.x, destination.y, destination.z, flag or 4294967295, entity, 4) -- 4294967295 = TraceFlags_IntersectEverything while true do local result, hit, endCoords, surfaceNormal, entityHit = GetShapeTestResult(rayHandle) if result ~= 1 then return hit, endCoords, surfaceNormal, entityHit end Wait(0) end end local function RaycastFromHeliCam(flag) local camCoords = GetCamCoord(camera.cam) local camRotation = camera.rotation or GetCamRot(camera.cam, 2) local destination = GetOffsetFromCoordsInWorldCoords(camCoords, camRotation, vector3(0.0, Config.TargetMaxReach, 0.0)) local hit, endCoords, _surfaceNormal, entityHit = Raycast(camCoords, destination, helicopter.entity, flag) return (hit == 1 and true) or false, endCoords, entityHit end local function LoadPostalFile(resource, file) local resourceState = GetResourceState(resource) if resourceState ~= "started" then print(string.format("^1ERROR: Postal resource %s was not started! It MUST be started before helicam for the postals to work! (Resource state: %s)^7", resource, resourceState)) Config.ShowPostalCodes = false else local jsonFile = LoadResourceFile(resource, file) if jsonFile == nil then print(string.format("^1ERROR: The script was not able to load postals file %s from postals resource %s! Make sure that the file is loaded in the postals resource.^7", file, resource)) else postals = json.decode(jsonFile) end end end local function SetCameraLabel() local label = nil if Config.ForceCameraLabel then label = Config.ForceCameraLabel else local livery = GetVehicleLivery(helicopter.entity) local heliConfig = Config.Helicopters[helicopter.model] if heliConfig and heliConfig.labels then if heliConfig.labels[livery] then label = heliConfig.labels[livery] else label = heliConfig.labels[0] end else label = Config.Helicopters.default.labels[0] end end SendNUIMessage({ action = 'setCameraLabel', label = label }) end local function CreateHelicopterCamera(heli, offset, rotation, camFov, ease, easeTime) local cam = CreateCam("DEFAULT_SCRIPTED_CAMERA", true) AttachCamToEntity(cam, heli, offset.x, offset.y, offset.z, true) SetCamRot(cam, 0.0, 0.0, rotation, 2) SetCamFov(cam, camFov) RenderScriptCams(true, ease, easeTime, false, false) return cam end local function GetHelicopterTimecycle(model) local heliConfig = Config.Helicopters[model] if heliConfig and heliConfig.timecycle ~= nil then return heliConfig.timecycle, heliConfig.timecycleStrength or Config.DefaultCameraTimecycleStrength end return Config.DefaultCameraTimecycle, Config.DefaultCameraTimecycleStrength end local function DeleteTablet() local playerPed = PlayerPedId() ClearPedSecondaryTask(playerPed) Wait(100) DetachEntity(tabletObj, true, false) DeleteEntity(tabletObj) tabletObj = nil end local function CreateTablet() local tablet = Config.Tablet RequestAnimDict(tablet.anim.dict) while not HasAnimDictLoaded(tablet.anim.dict) do Wait(0) end local playerPed = PlayerPedId() local boneIndex = GetPedBoneIndex(playerPed, tablet.bone) tabletObj = CreateObject(tablet.model, 0.0, 0.0, 0.0, true, true, false) AttachEntityToEntity(tabletObj, playerPed, boneIndex, tablet.offset.x, tablet.offset.y, tablet.offset.z, tablet.rotation.x, tablet.rotation.y, tablet.rotation.z, false, true, false, true, 1, true) TaskPlayAnim(playerPed, tablet.anim.dict, tablet.anim.name, 2.0, 2.0, -1, 49, 1.0, false, false, false) CreateThread(function() while inHeliCam do if not IsEntityPlayingAnim(playerPed, tablet.anim.dict, tablet.anim.name, 3) then TaskPlayAnim(playerPed, tablet.anim.dict, tablet.anim.name, 2.0, 2.0, -1, 49, 1.0, false, false, false) end Wait(500) end end) end local function GetStreetAndAreaNames(streetHash, coords) local street = GetStreetNameFromHashKey(streetHash) local area = GetLabelText(GetNameOfZone(coords.x, coords.y, coords.z)) return street, area end local function SetZoomBarLevel() local range = Config.Camera.Zoom.Max - Config.Camera.Zoom.Min local percentage = (fov - Config.Camera.Zoom.Min) / range * 100 percentage = (percentage - 100) *-1 -- Flip the values around SendNUIMessage({action = 'setZoomBarLevel', percentage = percentage}) end local function HandleZoomInput() if spotlight.adjustingBrightness or spotlight.adjustingRadius then return end if GetDisabledControlNormal(0, 40) ~= 0.0 then -- Zoom in fov = math.max(fov - Config.Camera.Zoom.Speed, Config.Camera.Zoom.Min) SetZoomBarLevel() elseif GetDisabledControlNormal(0, 41) ~= 0.0 then -- Zoom out fov = math.min(fov + Config.Camera.Zoom.Speed, Config.Camera.Zoom.Max) SetZoomBarLevel() end currentFov = GetCamFov(camera.cam) if math.abs(fov - currentFov) < 0.1 then fov = currentFov else SetCamFov(camera.cam, currentFov + (fov - currentFov) * 0.05) end end local function HandleMovementInput() if cameraLock.active then return end local axisX = GetDisabledControlNormal(0, 220) local axisY = GetDisabledControlNormal(0, 221) if axisX ~= 0.0 or axisY ~= 0.0 then local zoomValue = (1.0/(Config.Camera.Zoom.Max-Config.Camera.Zoom.Min))*(fov-Config.Camera.Zoom.Min) local rotation = camera.rotation local movementSpeed = (IsUsingKeyboard(1) and Config.Camera.MovementSpeed.Keyboard) or Config.Camera.MovementSpeed.Controller local newX = math.max(math.min(Config.Camera.RotationLimits.Up, rotation.x + axisY*-1.0*(movementSpeed)*(zoomValue+0.1)), Config.Camera.RotationLimits.Down) local newZ = rotation.z + axisX*-1.0*(movementSpeed)*(zoomValue+0.1) SetCamRot(camera.cam, newX, 0.0, newZ, 2) movementInput = true elseif movementInput then movementInput = false end end -- Sound -- local function LoadSounds() RequestAmbientAudioBank("POLICE_CHOPPER_CAM", false) Wait(100) for key, _soundId in pairs(sounds) do sounds[key] = GetSoundId() end end local function UnloadSounds() ReleaseAmbientAudioBank() for key, soundId in pairs(sounds) do if not HasSoundFinished(soundId) then StopSound(soundId) end ReleaseSoundId(soundId) sounds[key] = -1 end end local function EmitSound(soundId, soundName, audioRef, stopIfActive) if HasSoundFinished(soundId) then PlaySoundFrontend(soundId, soundName, audioRef or 0, true) elseif stopIfActive then StopSound(soundId) PlaySoundFrontend(soundId, soundName, audioRef or 0, true) end end local function SoundThread() CreateThread(function() while inHeliCam do if movementInput and not pauseMenu then EmitSound(sounds.turn, "COP_HELI_CAM_TURN") elseif not HasSoundFinished(sounds.turn) then StopSound(sounds.turn) end local fovDifference = math.abs(currentFov - fov) if fovDifference > 5.0 and not pauseMenu then EmitSound(sounds.zoom, "COP_HELI_CAM_ZOOM") else if not HasSoundFinished(sounds.zoom) then StopSound(sounds.zoom) end end Wait(100) end end) end -- Vision -- local function DoesAnyHeliHaveVisionOverwrite() for _hash, data in pairs(Config.Helicopters) do if data.nightvision or data.thermalvision then return true end end return false end local function CanHelicopterUseCameraVision(model, type) local configAllowed = (type == "nightvision" and Config.AllowNightVision) or Config.AllowThermal if (configAllowed and (Config.Helicopters[model] and Config.Helicopters[model][type] ~= false)) or (Config.Helicopters[model] and Config.Helicopters[model][type]) then return true end return false end local function DisableVision() SendNUIMessage({action = 'setVisionState', state = "HDEO"}) SetSeethrough(false) SetNightvision(false) visionState = 0 end local function EnableThermal() -- Reset the seethrough values SeethroughReset() -- Big thanks goes to BrD for making the black & white thermal posible! if Config.ThermalOptions.CustomColours then -- Some of these are relative to eachother SetVisualSettingFloat("seeThrough.ColorVisibleHot.red", Config.ThermalOptions.Colours.VisibleHot.R) SetVisualSettingFloat("seeThrough.ColorVisibleHot.green", Config.ThermalOptions.Colours.VisibleHot.G) SetVisualSettingFloat("seeThrough.ColorVisibleHot.blue", Config.ThermalOptions.Colours.VisibleHot.B) SetVisualSettingFloat("seeThrough.ColorVisibleWarm.red", Config.ThermalOptions.Colours.VisibleWarm.R) SetVisualSettingFloat("seeThrough.ColorVisibleWarm.green", Config.ThermalOptions.Colours.VisibleWarm.G) SetVisualSettingFloat("seeThrough.ColorVisibleWarm.blue", Config.ThermalOptions.Colours.VisibleWarm.B) SetVisualSettingFloat("seeThrough.ColorVisibleBase.red", Config.ThermalOptions.Colours.VisibleBase.R) SetVisualSettingFloat("seeThrough.ColorVisibleBase.green", Config.ThermalOptions.Colours.VisibleBase.G) SetVisualSettingFloat("seeThrough.ColorVisibleBase.blue", Config.ThermalOptions.Colours.VisibleBase.B) -- Colour of the far of fade as well as the sky SetVisualSettingFloat("seeThrough.ColorFar.red", Config.ThermalOptions.Colours.Far.R) SetVisualSettingFloat("seeThrough.ColorFar.green", Config.ThermalOptions.Colours.Far.G) SetVisualSettingFloat("seeThrough.ColorFar.blue", Config.ThermalOptions.Colours.Far.B) -- Colour of the ground SetVisualSettingFloat("seeThrough.ColorNear.red", Config.ThermalOptions.Colours.Near.R) SetVisualSettingFloat("seeThrough.ColorNear.green", Config.ThermalOptions.Colours.Near.G) SetVisualSettingFloat("seeThrough.ColorNear.blue", Config.ThermalOptions.Colours.Near.B) end -- Max amount of thickness we can see trough (not in m or ft, unsure how it's calculated by the game.) SeethroughSetMaxThickness(Config.ThermalOptions.MaxThickness) -- Set the amount of noise SeethroughSetNoiseAmountMin(Config.ThermalOptions.MinNoise) SeethroughSetNoiseAmountMax(Config.ThermalOptions.MaxNoise) -- Set how far we can see SeethroughSetFadeStartDistance(Config.ThermalOptions.FadeStart) SeethroughSetFadeEndDistance(Config.ThermalOptions.FadeEnd) -- Enable the seetrough effect (thermal vision) SetSeethrough(true) end local function CycleVision() if visionState == 0 then visionState = 1 if CanHelicopterUseCameraVision(helicopter.model, "nightvision") then local hour = GetClockHours() if Config.AllowNightVisionDuringDay or (hour > 20 or hour < 6) then EmitSound(sounds.thermal, "THERMAL_VISION_GOGGLES_ON_MASTER", 0, true) SendNUIMessage({action = 'setVisionState', state = "HDNV"}) SetNightvision(true) return end end end if visionState == 1 and CanHelicopterUseCameraVision(helicopter.model, "thermalvision") then SendNUIMessage({action = 'setVisionState', state = "HDIR"}) SetNightvision(false) EnableThermal() visionState = 2 return end DisableVision() end -- Camera Lock -- local function LockCamera(coords, entity) cameraLock.active = true movementInput = false local entityType = GetEntityType(entity) if entity and entityType ~= 0 then local offset = vector3(0.0, 0.0, 0.0) if not ShouldLockOntoCenter(entityType) then offset = GetOffsetFromEntityGivenWorldCoords(entity, coords.x, coords.y, coords.z) end PointCamAtEntity(camera.cam, entity, offset.x, offset.y, offset.z, true) cameraLock.prevType = cameraLock.type cameraLock.type = "entity" cameraLock.entity = entity cameraLock.coords = nil else -- We have to re-create the camera due to bug with the PointCamAtCoord/PointCamAtEntity functions regarding offsets. if cameraLock.prevType == "entity" then local offset = GetHeliCameraOffset(helicopter.model) local rotation = GetEntityRotation(helicopter.entity, 5).z local cam = CreateHelicopterCamera(helicopter.entity, offset, rotation, GetCamFov(camera.cam), false, 0) DestroyCam(camera.cam, true) camera.cam = cam end PointCamAtCoord(camera.cam, coords.x, coords.y, coords.z) cameraLock.prevType = cameraLock.type cameraLock.type = "coords" cameraLock.entity = nil cameraLock.coords = coords end local lockType = nil if cameraLock.type == "coords" then lockType = "GROUND" elseif entityType == 2 then lockType = "VEHICLE" elseif entityType == 1 then if GetPedType(cameraLock.entity) == 28 then lockType = "ANIMAL" else lockType = "PERSON" end else lockType = "UNKNOWN" end SendNUIMessage({ action = 'setCameraLockState', state = true, type = lockType }) end local function AttemptLockScanning(targetEntity) local count = 1 cameraLock.attempting = true SendNUIMessage({ action = 'startLockScanning' }) while true do Wait(200) local hit, hitCoords, hitEntity = RaycastFromHeliCam() if hit and hitEntity == targetEntity then count += 1 else count -= 1 end if not inHeliCam then return false end if Config.PlaySounds then EmitSound(sounds.scanLoop, "COP_HELI_CAM_SCAN_PED_LOOP") end SendNUIMessage({ action = 'updateLockScanning', value = count }) if count >= 11 then if Config.PlaySounds then if not HasSoundFinished(sounds.scanLoop) then StopSound(sounds.scanLoop) end PlaySoundFrontend(sounds.scanSuccess, "COP_HELI_CAM_SCAN_PED_SUCCESS", 0, true) end return true, hitCoords, hitEntity end if count <= -1 or not cameraLock.attempting then if Config.PlaySounds then if not HasSoundFinished(sounds.scanLoop) then StopSound(sounds.scanLoop) end PlaySoundFrontend(sounds.scanFailure, "COP_HELI_CAM_SCAN_PED_FAILURE", 0, true) end return false end end end local function AttemptCameraLock() local hit, hitCoords, hitEntity = RaycastFromHeliCam() if hit then if GetEntityType(hitEntity) ~= 0 then if Config.InstantCameraLock then LockCamera(hitCoords, hitEntity) else local success, coords, entity = AttemptLockScanning(hitEntity) if success then LockCamera(coords, entity) end cameraLock.attempting = false SendNUIMessage({ action = 'lockScanningFinished' }) end elseif Config.AllowCameraLockOnGround then LockCamera(hitCoords, hitEntity) end end end local function StopCameraLock() if camera.cam and cameraLock.active then StopCamPointing(camera.cam) end SendNUIMessage({ action = 'setCameraLockState', state = false, type = "NONE" }) -- Reset variables cameraLock.active = false cameraLock.prevType = cameraLock.type cameraLock.type = nil cameraLock.entity = nil cameraLock.coords = nil cameraLock.timeout = 0 cameraLock.progress = 0 end local function CheckCameraLock(hit, hitCoords, hitEntity) if cameraLock.active then local timeout = false if not hit then timeout = true elseif cameraLock.type == "coords" then local distToTarget = #(hitCoords-cameraLock.coords) if distToTarget > 1.0 then timeout = true end elseif cameraLock.type == "entity" then if hitEntity ~= cameraLock.entity and not HasEntityClearLosToEntity(helicopter.entity, cameraLock.entity, 4294967295) then timeout = true end end if timeout then cameraLock.timeout += 1 if Config.PlaySounds then EmitSound(sounds.bleep, "COP_HELI_CAM_BLEEP_TOO_FAR") end if cameraLock.timeout >= Config.CameraLockBreakTicks then StopCameraLock() end elseif cameraLock.timeout > 0 then cameraLock.timeout -= 1 end end end -- Rappel -- local function AttemptRappel(wasKeyPress) if not helicopter.entity then return end local heliConfig = Config.Helicopters[helicopter.model] if not DoesVehicleAllowRappel(helicopter.entity) or (heliConfig and heliConfig.disableRappelling) then DisplayNotification(Config.Localisation.Notification.CannotRappelFromHeli) return end local playerPed = PlayerPedId() local isInCorrectSeat = GetPedInVehicleSeat(helicopter.entity, 1) == playerPed or GetPedInVehicleSeat(helicopter.entity, 2) == playerPed if not isInCorrectSeat then DisplayNotification(Config.Localisation.Notification.CannotRappelFromSeat) return end local coords = GetEntityCoords(helicopter.entity) local foundHeight, groundZ = GetGroundZFor_3dCoord(coords.x, coords.y, coords.z, false) if not foundHeight or coords.z - groundZ > Config.MaxRappellingHight then DisplayNotification(Config.Localisation.Notification.ToHighToRappel) return end if wasKeyPress and (lastRappelKeyPress == 0 or (lastRappelKeyPress + Config.RappellingTimeout < GetGameTimer())) then lastRappelKeyPress = GetGameTimer() DisplayNotification(Config.Localisation.Notification.ConfirmRappel) else lastRappelKeyPress = 0 EmitSound(sounds.rappel, "SELECT", "HUD_FRONTEND_DEFAULT_SOUNDSET") DisplayNotification(Config.Localisation.Notification.Rappelling) TaskRappelFromHeli(playerPed, 1) end end -- Instructions -- local function AddInInstructionsControl(scaleform, index, control, text) BeginScaleformMovieMethod(scaleform, "SET_DATA_SLOT") ScaleformMovieMethodAddParamInt(index) ScaleformMovieMethodAddParamPlayerNameString(control) BeginTextCommandScaleformString("STRING") AddTextComponentSubstringPlayerName(text) EndTextCommandScaleformString() EndScaleformMovieMethod() end local function SetupInstructionsScaleform() instScaleform = RequestScaleformMovie("INSTRUCTIONAL_BUTTONS") while not HasScaleformMovieLoaded(instScaleform) do Wait(10) end BeginScaleformMovieMethod(instScaleform, "CLEAR_ALL") EndScaleformMovieMethod() BeginScaleformMovieMethod(instScaleform, "SET_CLEAR_SPACE") ScaleformMovieMethodAddParamInt(200) EndScaleformMovieMethod() -- Add Controls for index, button in pairs(Config.InstructionButtons) do AddInInstructionsControl(instScaleform, index, button.control, button.label) end -- Background colour BeginScaleformMovieMethod(instScaleform, "SET_BACKGROUND_COLOUR") ScaleformMovieMethodAddParamInt(0) -- Red ScaleformMovieMethodAddParamInt(0) -- Green ScaleformMovieMethodAddParamInt(0) -- Blue ScaleformMovieMethodAddParamInt(80) -- Alpha EndScaleformMovieMethod() BeginScaleformMovieMethod(instScaleform, "SET_BACKGROUND") EndScaleformMovieMethod() BeginScaleformMovieMethod(instScaleform, "DRAW_INSTRUCTIONAL_BUTTONS") EndScaleformMovieMethod() end local function InstructionsThread() CreateThread(function() while inHeliCam do DrawScaleformMovieFullscreen(instScaleform, 255, 255, 255, 255, 0) Wait(0) end end) end -- Spotlight -- local function DoesAnyHeliHaveSpotlightOverwrite() for _hash, data in pairs(Config.Helicopters) do if data.spotlight then return true end end return false end local function CanHelicopterUseSpotlight(model) if (Config.AllowSpotlight and (Config.Helicopters[model] and Config.Helicopters[model].spotlight ~= false)) or (Config.Helicopters[model] and Config.Helicopters[model].spotlight) then return true end return false end local function AdjustSpolightBrightness() CreateThread(function() while spotlight.adjustingBrightness do local newBrightness = spotlight.brightness if GetDisabledControlNormal(0, 40) ~= 0.0 then -- Scroll up newBrightness = math.min(newBrightness + Config.Spotlight.BrightnessIncrements, Config.Spotlight.MaxBrightness) elseif GetDisabledControlNormal(0, 41) ~= 0.0 then -- Scroll down newBrightness = math.max(newBrightness - Config.Spotlight.BrightnessIncrements, Config.Spotlight.MinBrightness) end spotlight.brightness = newBrightness Wait(0) end end) end local function AdjustSpolightRadius() CreateThread(function() while spotlight.adjustingRadius do local newRadius = spotlight.radius if GetDisabledControlNormal(0, 40) ~= 0.0 then -- Scroll up newRadius = math.min(newRadius + Config.Spotlight.RadiusIncrements, Config.Spotlight.MaxRadius) elseif GetDisabledControlNormal(0, 41) ~= 0.0 then -- Scroll down newRadius = math.max(newRadius - Config.Spotlight.RadiusIncrements, Config.Spotlight.MinRadius) end spotlight.radius = newRadius Wait(0) end end) end local function SpotlightThread() local direction = nil local position = nil local netId = helicopter.netId -- Reset spotlight brightness/radius spotlight.brightness = Config.Spotlight.DefaultBrightness spotlight.radius = Config.Spotlight.DefaultRadius CreateThread(function() while spotlight.active do local rotation = camera.rotation or GetCamRot(camera.cam, 2) direction = RotAnglesToVec(rotation) local camCoords = GetCamCoord(camera.cam) position = camCoords + direction DrawSpotLightWithShadow(position.x, position.y, position.z, direction.x, direction.y, direction.z, Config.Spotlight.Colour.R, Config.Spotlight.Colour.G, Config.Spotlight.Colour.B, Config.Spotlight.MaxDistance, spotlight.brightness, Config.Spotlight.Roundness, spotlight.radius, Config.Spotlight.Falloff, 0) Wait(0) end end) CreateThread(function() while spotlight.active do SynchroniseSpotlight({ position = position, direction = direction, brightness = spotlight.brightness, radius = spotlight.radius, helicopter = netId }) Wait(25) end SynchroniseSpotlight({ position = false, helicopter = netId }) end) end local function SpotlightCameraLockCheck() CreateThread(function() spotlight.cameraLockThread = true while spotlight.active and not inHeliCam do local hit, hitCoords, hitEntity = RaycastFromHeliCam() CheckCameraLock(hit, hitCoords, hitEntity) Wait(250) end spotlight.cameraLockThread = false end) end local function ToggleSpotlight() if not inHeliCam or pauseMenu then return end -- Check if our helicopter can use a spotlight if not CanHelicopterUseSpotlight(helicopter.model) then DisplayNotification(Config.Localisation.Notification.NoSpotlight) return end -- Emit toggle sound EmitSound(sounds.spotlight, "NAV_LEFT_RIGHT", "HUD_FRONTEND_DEFAULT_SOUNDSET") spotlight.active = not spotlight.active if spotlight.active then if not Config.MaxAmountOfSpotlights then SpotlightThread() return end if GlobalState.heliSpotlightsActive >= Config.MaxAmountOfSpotlights then spotlight.active = false DisplayNotification(Config.Localisation.Notification.SpotlightGlobalLimit) return end TriggerServerEvent('helicam:toggleSpotlight', true) SpotlightThread() elseif Config.MaxAmountOfSpotlights then TriggerServerEvent('helicam:toggleSpotlight', false) end end -- Spotlights that are not controled by us local function ForeignSpotlightThread() CreateThread(function() spotlight.isThreadActive = true while not IsTableEmpty(spotlights) do for _netId, data in pairs(spotlights) do DrawSpotLightWithShadow(data.position.x, data.position.y, data.position.z, data.direction.x, data.direction.y, data.direction.z, Config.Spotlight.Colour.R, Config.Spotlight.Colour.G, Config.Spotlight.Colour.B, Config.Spotlight.MaxDistance, data.brightness+0.0, Config.Spotlight.Roundness, data.radius+0.0, Config.Spotlight.Falloff, 0) end Wait(0) end spotlight.isThreadActive = false end) end AddStateBagChangeHandler('heliCamSpotlightData', nil, function(bagName, key, data, _unused, replicated) -- Ignore this if we are the camera operator if not data or (data.helicopter == helicopter.netId and camera.cam) then return end -- Turn off spotlight if not data.position then spotlights[data.helicopter] = nil return end spotlights[data.helicopter] = data if not spotlight.isThreadActive then ForeignSpotlightThread() end end) -- Blips -- local function ToggleMarker() local markerIndex = nil local hit, hitCoords = RaycastFromHeliCam() if not hit then return end local adjustedCoords = vector3(hitCoords.x, hitCoords.y, hitCoords.z + 0.25) for index, coords in pairs(markers) do local dist = #(coords - adjustedCoords) if dist < Config.Marker.Circle.Scale + 0.5 then markerIndex = index break end end if markerIndex then -- Remove Marker from table table.remove(markers, markerIndex) else -- If already max markers, remove the marker we created the longest ago if #markers >= Config.Marker.MaxAmount then table.remove(markers, 1) end -- Add marker markers[#markers+1] = adjustedCoords end SetHelicopterStateBag("heliCamMarkers", markers) end local function CreateMarkerBlip(coords, number) local blip = AddBlipForCoord(coords.x, coords.y, coords.z) SetBlipSprite(blip, Config.Marker.Blip.Sprite) SetBlipScale(blip, Config.Marker.Blip.Scale) SetBlipColour(blip, Config.Marker.Blip.Colour) if Config.Marker.Blip.Number then ShowNumberOnBlip(blip, number) end -- Set blip name BeginTextCommandSetBlipName("STRING") AddTextComponentSubstringPlayerName(Config.Localisation.Blip.Marker) EndTextCommandSetBlipName(blip) return blip end local function UpdateMarkerBlips() if markers == nil then return end if #markers < #markerBlips then for index, data in pairs(markerBlips) do if index > #markers then RemoveBlip(data.handler) markerBlips[index] = nil end end end for index, coords in pairs(markers) do if not markerBlips[index] then markerBlips[index] = {} markerBlips[index].handler = CreateMarkerBlip(coords, index) markerBlips[index].coords = coords elseif markerBlips[index].coords ~= coords then SetBlipCoords(markerBlips[index].handler, coords.x, coords.y, coords.z) ShowNumberOnBlip(markerBlips[index].handler, index) markerBlips[index].coords = coords end end end local function MarkersThread() CreateThread(function() isMarkersThreadActive = true while true do if markers == nil or #markers == 0 then break end local heliCoords = cache.helicopter.coords or GetEntityCoords(helicopter.entity) for index, coords in pairs(markers) do if #(heliCoords - coords) < Config.Marker.MaxDrawDistance then DrawMarker(Config.Marker.Circle.Type, coords.x, coords.y, coords.z, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, Config.Marker.Circle.Scale, Config.Marker.Circle.Scale, Config.Marker.Circle.Scale, Config.Marker.Circle.Colour.R, Config.Marker.Circle.Colour.G, Config.Marker.Circle.Colour.B, Config.Marker.Circle.Colour.A, false, true, 2, false, nil, nil, false) if Config.Marker.Number.Display then DrawMarker(index+10, coords.x, coords.y, coords.z, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, Config.Marker.Number.Scale, Config.Marker.Number.Scale, Config.Marker.Number.Scale, Config.Marker.Number.Colour.R, Config.Marker.Number.Colour.G, Config.Marker.Number.Colour.B, Config.Marker.Number.Colour.A, false, true, 2, false, nil, nil, false) end end end Wait(0) end isMarkersThreadActive = false end) end local function HandleTargetBlip(display) if display then local coords = display if DoesBlipExist(targetBlip.handler) then SetBlipCoords(targetBlip.handler, coords.x, coords.y, coords.z) else -- Create the blip targetBlip.handler = AddBlipForCoord(coords.x, coords.y, coords.z) SetBlipSprite(targetBlip.handler, Config.TargetBlip.Sprite) SetBlipColour(targetBlip.handler, Config.TargetBlip.Colour) BeginTextCommandSetBlipName("STRING") AddTextComponentSubstringPlayerName(Config.Localisation.Blip.Target) EndTextCommandSetBlipName(targetBlip.handler) end if not targetBlip.display then SetBlipDisplay(targetBlip.handler, 2) targetBlip.display = true end elseif display == false then if targetBlip.display then SetBlipDisplay(targetBlip.handler, 0) targetBlip.display = false end else RemoveBlip(targetBlip.handler) targetBlip.handler = nil targetBlip.display = false end end local function FetchAndApplyBlipStateBags(heliEntity) local heli = Entity(heliEntity) if heli then if heli.state.heliCamTargetBlip then HandleTargetBlip(heli.state.heliCamTargetBlip) end if heli.state.heliCamMarkers then markers = heli.state.heliCamMarkers UpdateMarkerBlips() MarkersThread() end end end local function RegisterBlipStateBags() if Config.TargetBlip.Display then blipStateBagHandlers.heliCamTargetBlip = AddStateBagChangeHandler('heliCamTargetBlip', nil, function(bagName, key, value, _unused, replicated) local entity = GetEntityFromStateBagName(bagName) local vehicle = helicopter.entity or GetVehiclePedIsIn(PlayerPedId(), false) if entity == vehicle then HandleTargetBlip(value) end end) end if Config.AllowMarkers then blipStateBagHandlers.heliCamMarkers = AddStateBagChangeHandler('heliCamMarkers', nil, function(bagName, key, value, _unused, replicated) local entity = GetEntityFromStateBagName(bagName) local vehicle = helicopter.entity or GetVehiclePedIsIn(PlayerPedId(), false) if entity == vehicle then markers = value UpdateMarkerBlips() if not isMarkersThreadActive then MarkersThread() end end end) end end local function UnregisterBlipStateBags() for _key, cookie in pairs(blipStateBagHandlers) do RemoveStateBagChangeHandler(cookie) end blipStateBagHandlers = {} end -- Threads & Main Functions -- local function MinimapHeadingThread() CreateThread(function() while inHeliCam do SetGameplayCamRelativeHeading(camera.heading * -1) Wait(0) end end) end local function ExitHeliCamera() inHeliCam = false cameraAction = true camera.rotation = nil -- Set camera avalible for others to use TriggerServerEvent('helicam:leaveCamera', helicopter.netId) -- Trigger event for other scripts to use TriggerEvent('helicam:leftCamera', helicopter.netId) -- Close NUI SendNUIMessage({ action = 'close' }) -- Disable night-/therma-vision if enabled if visionState ~= 0 then DisableVision() end -- Remove tablet object if tabletObj then DeleteTablet() end if Config.InstructionButtons then SetScaleformMovieAsNoLongerNeeded(instScaleform) end if not submix and Config.NoSubmixInCamera then EnableSubmix() end local timecycleName, _timecycleStrength = GetHelicopterTimecycle(helicopter.model) if timecycleName then ClearTimecycleModifier() end -- Reset gameplay camera, stop rendering and remove helicopter camera SetGameplayCamRelativeHeading(0) RenderScriptCams(false, Config.CameraTransition, Config.CameraTransitionTime, false, false) if Config.CameraTransition then Wait(Config.CameraTransitionTime) end if not spotlight.active then StopCameraLock() DestroyCam(camera.cam, false) camera.cam = nil elseif not spotlight.cameraLockThread then SpotlightCameraLockCheck() end -- Unload sounds if Config.PlaySounds then UnloadSounds() -- Exit sound EmitSound(sounds.exit, "BACK", "HUD_FRONTEND_DEFAULT_SOUNDSET") end -- Toggle radar on again if Config.HideMinimap then DisplayRadar(true) end -- Reset varaibles cameraAction = false end local function OnLeftHelicopter() markers = {} HandleTargetBlip(nil) UpdateMarkerBlips() UnregisterBlipStateBags() if inHeliCam then ExitHeliCamera() end -- Reset camera variables StopCameraLock() -- Destroy camera DestroyCam(camera.cam, false) camera.cam = nil -- Removes audio submix if it was enabled if submix then DisableSubmix() end -- Remove spotlight if spotlight.active then spotlight.active = false end helicopter = {} end local function InHelicopterThread() CreateThread(function() local heli = GetVehiclePedIsIn(PlayerPedId(), false) while heli == helicopter.entity do Wait(250) heli = GetVehiclePedIsIn(PlayerPedId(), false) end OnLeftHelicopter() end) end local function OnEnteredVehicle(vehicle) local model = GetEntityModel(vehicle) local hasCamera = DoesHelicopterHaveCamera(model, vehicle) if hasCamera then CreateThread(function() if helicopter.entity then while helicopter.entity ~= nil and helicopter.entity ~= 0 do Wait(0) end Wait(100) end helicopter.entity = vehicle helicopter.netId = VehToNet(vehicle) helicopter.model = model RegisterBlipStateBags() FetchAndApplyBlipStateBags(vehicle) InHelicopterThread() end) end if Config.UseSubmix then EnableSubmix() if not hasCamera then CreateThread(function() local playerPed = PlayerPedId() while IsPedInAnyHeli(playerPed) or IsPedInAnyPlane(playerPed) do Wait(250) end -- Removes audio submix if it was enabled if submix then DisableSubmix() end end) end end end local function CollectAndSendData() local info = {} local data = {} data.target = {} data.helicopter = {} data.helicopter.speed = GetEntitySpeed(helicopter.entity) data.helicopter.coords = GetEntityCoords(helicopter.entity) data.helicopter.heading = GetEntityHeading(helicopter.entity) data.target.numberplate = false if Config.TimeFormat == 1 then local hour = GetClockHours() local minute = GetClockMinutes() info.time = ("%.2d"):format((hour == 0) and 12 or hour) .. ":" .. ("%.2d"):format(minute) end local setData = {} if data.helicopter.speed ~= cache.helicopter.speed then setData['hi-speed'] = string.format("%.0f", data.helicopter.speed * Units.Speed.Conversion) cache.helicopter.speed = data.helicopter.speed end if data.helicopter.coords ~= cache.helicopter.coords then setData['hi-altitude'] = string.format("%.0f", data.helicopter.coords.z * Units.Altitude.Conversion) cache.helicopter.coords = data.helicopter.coords end if data.helicopter.heading ~= cache.helicopter.heading then setData['hi-heading'] = string.format("%.0f", data.helicopter.heading) cache.helicopter.heading = data.helicopter.heading end if camera.pitch ~= cache.camera.pitch then setData['camera-pitch'] = string.format("%.0f", (camera.pitch - 90) * -1).."°" cache.camera.pitch = camera.pitch end if camera.heading ~= cache.camera.heading then setData['camera-heading'] = string.format("%.0f", camera.heading).."°" cache.camera.heading = camera.heading end if camera.bearing ~= cache.camera.bearing then setData['bearing-text'] = string.format("%.0f", camera.bearing).."°T" cache.camera.bearing = camera.bearing end local hit, hitCoords, hitEntity = RaycastFromHeliCam() if cameraLock.active then CheckCameraLock(hit, hitCoords, hitEntity) hitEntity = cameraLock.entity end if hit then data.target.elevation = string.format("%.0f", hitCoords.z * Units.TargetElevation.Conversion) if data.target.elevation ~= cache.target.elevation then setData['ta-elevation'] = data.target.elevation cache.target.elevation = data.target.elevation end if (hitEntity and GetEntityType(hitEntity) ~= 0) or cameraLock.active then data.target.heading = GetEntityHeading(hitEntity) data.target.speed = string.format("%.0f", GetEntitySpeed(hitEntity) * Units.TargetSpeed.Conversion) if IsEntityAVehicle(hitEntity) and ((Config.OnlyShowPlateIfLocked and cameraLock.active) or not Config.OnlyShowPlateIfLocked) then local success, plate = GetVehicleNumberPlate(hitEntity, GetEntityRotation(hitEntity), camera.rotation or GetCamRot(camera.cam, 2)) if success then data.target.numberplate = plate end end data.target.heading = string.format("%.0f", data.target.heading) elseif movementInput and cache.target.position ~= nil then data.target.speed = string.format("%.0f", #(cache.target.position - hitCoords) * Units.TargetSpeed.Conversion) data.target.heading = string.format("%.0f", GetHeadingBetweenCoords(cache.target.position, hitCoords)) end if hitCoords ~= cache.target.position then SetHelicopterStateBag('heliCamTargetBlip', hitCoords) cache.target.position = hitCoords end if data.target.speed ~= cache.target.speed then cache.target.speed = data.target.speed if data.target.speed == nil then data.target.speed = "---" end setData['ta-speed'] = data.target.speed end if data.target.heading ~= cache.target.heading then cache.target.heading = data.target.heading if data.target.heading == nil then data.target.heading = "---" end setData['ta-heading'] = data.target.heading end data.target.distance = #(hitCoords - data.helicopter.coords) if data.target.distance ~= cache.target.distance then local decimals = (Units.TargetDistance.Type == "MI" and "%.2f") or "%.0f" setData['ta-distance'] = string.format(decimals, data.target.distance * Units.TargetDistance.Conversion) cache.target.distance = data.target.distance end else if cache.target.speed ~= "---" then setData['ta-speed'] = "---" cache.target.speed = "---" end if cache.target.heading ~= "---" then setData['ta-heading'] = "---" cache.target.heading = "---" end if cache.target.elevation ~= "---" then setData['ta-elevation'] = "---" cache.target.elevation = "---" end if cache.target.distance ~= "---" then setData['ta-distance'] = "---" cache.target.distance = "---" end local heli = Entity(helicopter.entity) if not heli or (heli and heli.state.heliCamTargetBlip) then SetHelicopterStateBag('heliCamTargetBlip', false) end end -- Send number plate if it's different from last time we send it to the NUI if cache.target.numberplate ~= (data.target.numberplate or false) then info.numberplate = data.target.numberplate cache.target.numberplate = data.target.numberplate end if Config.ShowLatitudeLongitude then local latitude = GetCartesianCoords(data.helicopter.coords.x) local longitude = GetCartesianCoords(data.helicopter.coords.y) if data.helicopter.latitude ~= latitude then setData['hi-latitude'] = latitude cache.helicopter.latitude = latitude end if data.helicopter.longitude ~= longitude then setData['hi-longitude'] = longitude cache.helicopter.longitude = longitude end if hit then local targetLatitude = GetCartesianCoords(hitCoords.x) local targetLongitude = GetCartesianCoords(hitCoords.y) if data.helicopter.latitude ~= targetLatitude then setData['ta-latitude'] = targetLatitude cache.target.latitude = targetLatitude end if data.helicopter.longitude ~= targetLongitude then setData['ta-longitude'] = targetLongitude cache.target.longitude = targetLongitude end else if data.helicopter.latitude ~= "---" then setData['ta-latitude'] = "---" cache.target.latitude = "---" end if data.helicopter.longitude ~= "---" then setData['ta-longitude'] = "---" cache.target.longitude = "---" end end else -- Street name and area local streetHash, _crossingHash = GetStreetNameAtCoord(data.helicopter.coords.x, data.helicopter.coords.y, data.helicopter.coords.z) if streetHash ~= cache.helicopter.street then local street, area = GetStreetAndAreaNames(streetHash, data.helicopter.coords) setData['hi-street'] = street.." - "..area cache.helicopter.street = streetHash end if hit then local targetStreetHash, _targetCrossingHash = GetStreetNameAtCoord(hitCoords.x, hitCoords.y, hitCoords.z) if targetStreetHash ~= cache.target.street then local targetStreet, targetArea = GetStreetAndAreaNames(targetStreetHash, hitCoords) setData['ta-street'] = targetStreet.." - "..targetArea cache.target.street = targetStreetHash end elseif cache.target.street ~= "---" then setData['ta-street'] = "---" cache.target.street = "---" end end if not IsTableEmpty(info) or not IsTableEmpty(setData) then SendNUIMessage({ action = 'updateData', info = info, set = setData }) end end local function UpdateUIHeadingPitchAndBearing() local rotation = GetCamRot(camera.cam, 2) local bearing = string.format("%.0f", RotationToHeading(rotation.z)) local pitch = (rotation.x * -1) + 90.0 local heading = (rotation.z * -1) + GetEntityHeading(helicopter.entity) if heading > 360 then heading = heading - 360 end if math.abs(camera.pitch - pitch) > 0.1 or math.abs(camera.heading - heading) > 0.1 then SendNUIMessage({ action = 'updateDataFrame', pitch = pitch, heading = heading, bearing = bearing }) camera.pitch = pitch camera.heading = heading camera.bearing = bearing end camera.rotation = rotation end local function PrimaryThread() UpdateUIHeadingPitchAndBearing() CreateThread(function() while inHeliCam do if not pauseMenu then -- Camera Heading, Pitch and Bearing UpdateUIHeadingPitchAndBearing() -- Handle inputs HandleZoomInput() HandleMovementInput() -- Disable game inputs for _index, control in pairs(controlActions) do DisableControlAction(0, control, true) end else Wait(100) end Wait(0) end end) end local function DisplayPostalLoop() CreateThread(function() while inHeliCam and postalsActive do for _index, data in pairs(displayPostals) do DrawText3D(data.coords, data.code) end Wait(0) end displayPostals = {} end) end local function PostalLoop() CreateThread(function() while inHeliCam and postalsActive do local coords = cache.target.position and cache.target.position.xy or cache.helicopter.coords.xy local inDistance = {} for _index, data in pairs(postals) do local postalCoords = data.coords and data.coords.xy or vector2(data.x, data.y) local dist = #(coords - postalCoords) if dist < 500.0 then if data.coords then inDistance[#inDistance+1] = { coords = data.coords, code = data.code, dist = dist } else local success, groundZ = GetGroundZFor_3dCoord(data.x, data.y, cache.helicopter.coords.z, false) if success then data.coords = vector3(data.x, data.y, groundZ) -- Cache's the z coord data.x = nil data.y = nil inDistance[#inDistance+1] = { coords = data.coords, code = data.code, dist = dist } end end end end table.sort(inDistance, function(p1, p2) return p1.dist < p2.dist end) displayPostals = {} for i = 1, 50 do displayPostals[i] = inDistance[i] end Wait(500) end end) DisplayPostalLoop() end local function SecondaryThread() CollectAndSendData() SendNUIMessage({ action = 'open' }) CreateThread(function() while true do if not inHeliCam then return end if IsEntityDead(PlayerPedId()) then ExitHeliCamera() return end if IsPauseMenuActive() then if not pauseMenu then pauseMenu = true SendNUIMessage({ action = 'close' }) end else CollectAndSendData() -- Sets camera depth of field local dist = type(cache.target.distance) == "number" and cache.target.distance or 500.0 SetCamDofFocusDistanceBias(camera.cam, dist) if pauseMenu then pauseMenu = false SendNUIMessage({ action = 'open' }) end end Wait(250) end end) end local function UseHeliCamera() if helicopter.model == nil or helicopter.entity == nil then print("^1ERROR: helicopter model or entity was nil, this is fatal and will cause issues!^7") end cameraAction = true inHeliCam = true SetCameraLabel() if Config.PlaySounds then LoadSounds() -- Loads the sounds used when in the camera if Config.PlayCameraMovementSounds then SoundThread() -- Handles sounds, runs every 100ms end -- Enter sound EmitSound(sounds.enter, "SELECT", "HUD_FRONTEND_DEFAULT_SOUNDSET") end if not camera.cam then fov = Config.Camera.Zoom.Max local offset = GetHeliCameraOffset(helicopter.model) local rotation = GetEntityRotation(helicopter.entity, 5).z camera.cam = CreateHelicopterCamera(helicopter.entity, offset, rotation, 50.0, Config.CameraTransition, Config.CameraTransitionTime) if Config.CameraTransition then Wait(Config.CameraTransitionTime) end else RenderScriptCams(true, Config.CameraTransition, Config.CameraTransitionTime, false, false) end local timecycleName, timecycleStrength = GetHelicopterTimecycle(helicopter.model) if timecycleName then SetTimecycleModifier(timecycleName) SetTimecycleModifierStrength(timecycleStrength) end -- Trigger event for other scripts to use TriggerEvent('helicam:enteredCamera', helicopter.netId) -- Reset the zoom bar SetZoomBarLevel() if Config.ShowInstructions then SetupInstructionsScaleform() InstructionsThread() end if Config.HideMinimap then DisplayRadar(false) else MinimapHeadingThread() end if Config.UseSubmix and Config.NoSubmixInCamera then DisableSubmix() end PrimaryThread() -- Handles controls and other stuff thats needs to be run every frame SecondaryThread() -- Handles "everything else", runs once every 250ms to save resources if Config.ShowPostalCodes then PostalLoop() end if Config.UseAnimProp then CreateTablet() end cameraAction = false end -- Events -- RegisterNetEvent('helicam:enterCamera') AddEventHandler('helicam:enterCamera', function(state) if state then UseHeliCamera() else DisplayNotification(Config.Localisation.Notification.CameraInUse) end end) AddEventHandler('gameEventTriggered', function(event, args) if event == "CEventNetworkPlayerEnteredVehicle" then if args[1] == PlayerId() then OnEnteredVehicle(args[2]) end end end) -- Commands & Key Mapping -- RegisterKeyMapping('helicam', Config.Localisation.KeyMapping.ToggleCam, Config.Keybinds.ToggleCam.Type, Config.Keybinds.ToggleCam.Key) RegisterCommand('helicam', function() if cameraAction or pauseMenu then return end if not inHeliCam then local playerPed = PlayerPedId() if IsPedInAnyHeli(playerPed) or IsPedInAnyPlane(playerPed) then local canUseCamera, message = CanPlayerUseCamera(playerPed) if canUseCamera then TriggerServerEvent('helicam:enterCamera', helicopter.netId) elseif message then DisplayNotification(Config.Localisation.Notification[message]) end end else ExitHeliCamera() end end, false) if Config.AllowCameraLock then RegisterKeyMapping('+helicam_lock', Config.Localisation.KeyMapping.AttemptLock, Config.Keybinds.AttemptLock.Type, Config.Keybinds.AttemptLock.Key) RegisterCommand('+helicam_lock', function() if not inHeliCam or pauseMenu then return end if not cameraLock.active then AttemptCameraLock() else StopCameraLock() end end, false) RegisterCommand('-helicam_lock', function() cameraLock.attempting = false end, false) end if Config.AllowNightVision or Config.AllowThermal or DoesAnyHeliHaveVisionOverwrite() then RegisterKeyMapping('helicam_cycle_vision', Config.Localisation.KeyMapping.CycleVision, Config.Keybinds.CycleVision.Type, Config.Keybinds.CycleVision.Key) RegisterCommand('helicam_cycle_vision', function() if inHeliCam and not pauseMenu then CycleVision() end end, false) end if Config.AllowMarkers then RegisterKeyMapping('helicam_toggle_marker', Config.Localisation.KeyMapping.ToggleMarker, Config.Keybinds.ToggleMarker.Type, Config.Keybinds.ToggleMarker.Key) RegisterCommand('helicam_toggle_marker', function() if inHeliCam and not pauseMenu then ToggleMarker() end end, false) end if Config.AllowRappelling then RegisterKeyMapping('+rappel', Config.Localisation.KeyMapping.Rappel, Config.Keybinds.Rappel.Type, Config.Keybinds.Rappel.Key) RegisterCommand('rappel', function() AttemptRappel(false) end, false) RegisterCommand('+rappel', function() AttemptRappel(true) end, false) RegisterCommand('-rappel', function() -- This is just a place holder to prevent "unknown command" messages in chat end, false) end if Config.AllowSpotlight or DoesAnyHeliHaveSpotlightOverwrite() then -- Toggle spotlight RegisterKeyMapping('helispotlight', Config.Localisation.KeyMapping.Spotlight, Config.Keybinds.Spotlight.Type, Config.Keybinds.Spotlight.Key) RegisterCommand('helispotlight', function() ToggleSpotlight() end, false) -- Adjusting spoltight brightness RegisterKeyMapping('+adjust_heli_spotlight_brightness', Config.Localisation.KeyMapping.SpotlightBrightness, Config.Keybinds.SpotlightBrightness.Type, Config.Keybinds.SpotlightBrightness.Key) RegisterCommand('+adjust_heli_spotlight_brightness', function() if inHeliCam and spotlight.active and not pauseMenu then if spotlight.adjustingBrightness then return end spotlight.adjustingBrightness = true AdjustSpolightBrightness() end end, false) RegisterCommand('-adjust_heli_spotlight_brightness', function() spotlight.adjustingBrightness = false end, false) -- Adjusting spoltight size/radius RegisterKeyMapping('+adjust_heli_spotlight_radius', Config.Localisation.KeyMapping.SpotlightRadius, Config.Keybinds.SpotlightRadius.Type, Config.Keybinds.SpotlightRadius.Key) RegisterCommand('+adjust_heli_spotlight_radius', function() if inHeliCam and spotlight.active and not pauseMenu then if spotlight.adjustingRadius then return end spotlight.adjustingRadius = true AdjustSpolightRadius() end end, false) RegisterCommand('-adjust_heli_spotlight_radius', function() spotlight.adjustingRadius = false end, false) end if Config.ShowPostalCodes then RegisterKeyMapping('helipostals', Config.Localisation.KeyMapping.Postals, Config.Keybinds.Postals.Type, Config.Keybinds.Postals.Key) RegisterCommand('helipostals', function() if inHeliCam then postalsActive = not postalsActive if postalsActive then PostalLoop() end end end, false) end -- Init -- CreateThread(function() local conversions = { -- Speed KTS = 1.943844, -- Knots per hour MPH = 2.236936, -- Miles per hour KMH = 3.6, -- Kilometers per hour MPS = 1.0, -- Meters per second FPS = 3.280840, -- Feet per second -- Distance FT = 3.2808399, -- Feet M = 1.0, -- Meters MI = 0.00062137 -- Miles } for index, unit in pairs(Config.Units) do Units[index] = { Type = unit or 'M', Conversion = conversions[unit] or conversions['M'] } end if Config.DisablePoliceScanner then SetAudioFlag('PoliceScannerDisabled', true) end if Config.DisableFlightMusic then SetAudioFlag("DisableFlightMusic", true) end if Config.AddChatSuggestions then TriggerEvent('chat:addSuggestion', '/helicam', Config.Localisation.ChatSuggestions.ToggleCamera) TriggerEvent('chat:addSuggestion', '/rappel', Config.Localisation.ChatSuggestions.Rappel) end if Config.ShowPostalCodes then LoadPostalFile(Config.PostalResource, Config.PostalFile) end if type(Config.DefaultCameraTimecycleStrength) ~= "number" then print(string.format("^1ERROR: Config.DefaultCameraTimecycleStrength is invalid, it needs to be a number! Current type: %s, current value: %s.^7", type(Config.DefaultCameraTimecycleStrength), Config.CameraTimecycleStrength)) Config.DefaultCameraTimecycleStrength = 0.5 end Wait(2500) SendNUIMessage({ action = 'setConfigData', set = { ['hi-speed-unit'] = Config.Units.Speed, ['hi-altitude-unit'] = Config.Units.Altitude, ['ta-speed-unit'] = Config.Units.TargetSpeed, ['ta-elevation-unit'] = Config.Units.TargetElevation, ['ta-distance-unit'] = Config.Units.TargetDistance }, showLatitudeLongitude = Config.ShowLatitudeLongitude, showLicensePlate = Config.ShowLicensePlate, timeFormat = Config.TimeFormat, dateFormat = Config.DateFormat, hideMinimap = Config.HideMinimap, showInstructions = Config.ShowInstructions, zoomBarOffset = Config.ZoomBarOffset }) end) -- Exports -- local function InHelicam() return inHeliCam end exports('InHelicam', InHelicam) -- Debugging Fix (if you restart the script while in a helicopter) local currentResourceName = GetCurrentResourceName() AddEventHandler('onResourceStart', function(resourceName) if currentResourceName ~= resourceName then return end local vehicle = GetVehiclePedIsIn(PlayerPedId(), false) if vehicle ~= 0 then OnEnteredVehicle(vehicle) end end) AddEventHandler('onResourceStop', function(resourceName) if currentResourceName ~= resourceName then return end if DoesEntityExist(tabletObj) then ClearPedSecondaryTask(PlayerPedId()) DetachEntity(tabletObj, true, false) DeleteEntity(tabletObj) tabletObj = nil end if inHeliCam then local heli = Entity(helicopter.entity) if heli and heli.state.heliCamInUse then SetHelicopterStateBag('heliCamInUse', false) end end if submix then DisableSubmix() end end)