diff --git a/resources/helicam/.fxap b/resources/helicam/.fxap new file mode 100644 index 000000000..a301abe2a Binary files /dev/null and b/resources/helicam/.fxap differ diff --git a/resources/helicam/client.lua b/resources/helicam/client.lua new file mode 100644 index 000000000..88a03f9b5 --- /dev/null +++ b/resources/helicam/client.lua @@ -0,0 +1,2029 @@ +-- 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) diff --git a/resources/helicam/config.lua b/resources/helicam/config.lua new file mode 100644 index 000000000..6fd36cc7c --- /dev/null +++ b/resources/helicam/config.lua @@ -0,0 +1,359 @@ +Config = {} + +Config.TimeFormat = 0 -- 0 = ZULU (UTC), 1 = In Game Time, 2 = OS TIME (Local Time) +Config.DateFormat = 1 -- 0 = MM/DD/YY, 1 = DD/MM/YY, 2 = YY/MM/DD, 3 = DD-Mon-YYYY + +Config.PlaySounds = true +Config.PlayCameraMovementSounds = true -- If true, sounds will be played when you move the helicopter camera +Config.DisablePoliceScanner = true -- If true, disables the police radio/scanner/dispatch sounds. +Config.DisableFlightMusic = true -- If true, the ambiant flight music will be disabled. + +Config.CameraTransition = false -- If true, the camera will ease when you enter/exit the camera +Config.CameraTransitionTime = 1000 -- The time the transition/ease will take. + +Config.DefaultCameraTimecycle = false -- "modiferName" or false. If set to a modifer name (string) then all helicopters will have this timecycle effect unless spesifed under the model inside the Config.Helicopters table ("CAMERA_BW", "CAMERA_secuirity", "secret_camera" or any valid timecycle: https://wiki.rage.mp/index.php?title=Timecycle_Modifiers) +Config.DefaultCameraTimecycleStrength = 0.5 -- number, 0.0 to 1.0. + +Config.ShowLatitudeLongitude = false -- If set to true, it shows latitude/longitude, otherwise it shows the street and area +Config.TargetMaxReach = 424.0 -- The longest distance we can target an entity (424 is the regular culling dist for a player, you won't ever be able to practically reach this long anyway) + +Config.ForceCameraLabel = false -- false or "Label" +Config.CanUseAnyHelicopter = false -- If true, any helicopter can be used, if false, only helicopters in the Config.Helicopters table are allowed. + +Config.AllowCameraLock = true -- If true, the camera can lock onto the ground/targets +Config.InstantCameraLock = false -- If true, the camera will immediately lock on to the target instead of requiering a grace period +Config.AllowCameraLockOnGround = true -- If true, the camera can lock onto the ground/buildings etc. +Config.CameraLockBreakTicks = 3 -- The maximum amount of ticks the camera can lose visual of the point/person/vehicle before it breaks. +-- If true, when locking onto an entity it will lock to the center of it, if false the camera locks onto the part of the entity you were aiming at right before locking. +Config.LockOntoCenter = { + Peds = true, -- It has a tendency to not work perfectly when this is false. + Vehicles = false +} + +Config.ShowLicensePlate = true -- (LPL) +Config.CheckLicensePlateAngle = true -- If true, it checks the angle between the helicopter camera and the plate. This reasults in player beeing unable to get a license plate read when the camera can't clearly see the plate. +Config.OnlyShowPlateIfLocked = false -- If true, the plate will only be showed if we have a lock on the vehicle. + +Config.WhitelistedJobs = false -- If false, the script doesn't check for jobs. If you want to check for jobs add a table where the job is the key, and the value is the job level like so: Config.WhitelistedJobs = { ['police'] = 2, ['ambulance'] = 5 } +Config.ShowMessageIfWrongJob = true -- If true, it will display an error message of you attempt to enter the camer without the right job, if false no message, just won't allow you to enter + +-- false = Anyone can use the camera (including the pilot), 1 = Any passanger (not pilot) can use the camera, 2 = Only rear passangers can use the camera +Config.PassengerOnly = false + +Config.ShowPostalCodes = false -- Shows postals while in the camera (This is a little resource heavy) +Config.PostalResource = "nearest-postal" -- The resource the postal file is located in (resource must be loaded before helicam, and the files must be formated like nearest-postal) +Config.PostalFile = "new-postals.json" -- MUST be a JSON file! (Note: The file must be loaded inside the fxmanifest of Config.PostalResource for the helicam script to be able to read it) + +-- Adds a audio sumbix while in a helicopter/plane (noise suppression) (NOTE: Get's applied to all helicopters/planes, not just the ones with a camera) +Config.UseSubmix = false +Config.NoSubmixInCamera = false -- Only applys if Config.UseSubmix is set to true, if true, this will stop the submix when you are using the camera + +-- If true, players will be able to rappel from helicopters that support it. +Config.AllowRappelling = true +Config.RappellingTimeout = 1000 -- The time in ms that you have to press the rappel button again (so people don't accidentally rappel) +Config.MaxRappellingHight = 35.0 -- At any higher then 35.0 players might fall off the rope as the game has a hardcoded cap around 30.0 - 40.0 meters. + +-- If the script should add chat suggestions for some of the chat commands. +Config.AddChatSuggestions = true + +-- Spotlight +Config.AllowSpotlight = true +Config.MaxAmountOfSpotlights = false -- false or number, set's the maximum amount of spotlights that are allowed on the server at any given time (global limit) +Config.Spotlight = { + Colour = { R = 255, G = 255, B = 255 }, -- The RGB colour values of the spotlight, you can for example make it slightly more blue like so: { R = 220, G = 220, B = 255 } + MaxDistance = 600.0, -- The maximum distance the spotlight will glow + DefaultBrightness = 5.0, -- The defult brightness of the spotlight + MinBrightness = 0.5, -- The minimum brightness of the spotlight + MaxBrightness = 15.0, -- The maximum brightness of the spotlight + BrightnessIncrements = 0.75, -- How much to change the brightness of the spotlight when adjusting it + Roundness = 2.0, -- The "roundness" of the spotlight + DefaultRadius = 10.0, -- The defult radius/size of the spotlight + MinRadius = 5.0, -- The minimum radius of the spotlight + MaxRadius = 15.0, -- The maximum radius of the spotlight + RadiusIncrements = 0.75, -- How much to change the radius of the spotlight when adjusting it + Falloff = 200.0, -- +} + +Config.HideMinimap = false +Config.ZoomBarOffset = 20 -- The offset in % from the bottom of the screen the zoom bar will be when the minimap is enabled. (If you have something on the top of your minimap then setting it to 25 usally works well) +Config.TargetBlip = { + Display = true, + Sprite = 390, + Colour = 40 +} + +Config.AllowNightVision = true +Config.AllowNightVisionDuringDay = false -- If the night vision can be enabeld during the day or not. +Config.AllowThermal = true +Config.ThermalOptions = { + MaxThickness = 1.0, -- 1.0 = Default GTA, can't see trough much at all. 20.0 = Able to see trough most thin walls, however won't be able to see trough the ground or multiple/thick walls. + MinNoise = 0.0, -- The minimum amount of background noise + MaxNoise = 0.1, -- The maximum amount of background noise + FadeStart = 5000.0, -- How far away (meters) before the "background" fade starts + FadeEnd = 6000.0, -- How far away (meters) before the "background" fade becomes solid + CustomColours = true, -- If we should use custom colours for the thermal camera (so it becomes black & white for example). (Colours are set below under Config.ThermalOptions.Colours) + Colours = { + VisibleHot = { R = 0.80, G = 0.80, B = 0.80 }, + VisibleWarm = { R = 0.80, G = 0.80, B = 0.80 }, + VisibleBase = { R = 0.80, G = 0.80, B = 0.80 }, + Far = { R = 0.20, G = 0.20, B = 0.20 }, + Near = { R = 0.15, G = 0.15, B = 0.15 } + } +} + +Config.AllowMarkers = true +Config.Marker = { + MaxAmount = 9, -- Above 9 the number markers should be disabled. + MaxDrawDistance = 1000.0, -- The furthest distance a marker will be drawn at. + Circle = { + Type = 23, + Scale = 8.0, + Colour = { R = 230, G = 50, B = 50, A = 200 } -- { R = 110, G = 160, B = 230, A = 200 } + }, + Number = { + Display = true, -- Set this to false if you want the max amount of markers the be above 9 + Scale = 6.0, + Colour = { R = 230, G = 50, B = 50, A = 200 } + }, + Blip = { + Display = true, -- Whether to display marker blips + Number = true, -- Whether to display the number on the blip or not (99 is cap.) + Sprite = 57, + Scale = 0.75, + Colour = 1, -- Red + } +} + +Config.UseAnimProp = true -- If true, players will have a tablet in their hands while using the camera +Config.Tablet = { + model = -1585232418, -- prop_cs_tablet + anim = { + dict = "amb@world_human_seat_wall_tablet@female@base", + name = "base" + }, + bone = 57005, -- SKEL_R_Hand + offset = vector3(0.17, 0.10, -0.13), + rotation = vector3(20.0, 180.0, 180.0) +} + +Config.ShowInstructions = false -- If true, instructions will be showns while you are in the camera +-- Used this website to get the controls (~INPUT_5D25DCCD~ for example): http://tools.povers.fr/hashgenerator/ +-- 0 = On the right, 3+ = on the left. +Config.InstructionButtons = { + -- [0] = { control = "~INPUT_5D25DCCD~", label = "Adjust Spotlight Brightness (scroll +)" }, + -- [1] = { control = "~INPUT_F8C9FB3A~", label = "Adjust Spotlight Radius (scroll +)" }, + -- [2] = { control = "~INPUT_DB481F5~", label = "Lock Camera" }, -- This doesn't work for some reason... + [0] = { control = "~INPUT_662F7BF5~", label = "Add/Remove Marker" }, + [1] = { control = "~INPUT_DB471A88~", label = "Cycle Vision" }, + [2] = { control = "~INPUT_51D50495~", label = "Spotlight" }, + [3] = { control = "~INPUT_5D25DCCD~", label = "Exit Camera" } +} + +-- Speed Units: +-- KTS = Knots (nautical miles per hour) +-- MPH = Miles per hour +-- KMH = Kilometers per hour +-- MPS = Meters per second +-- FPS = Feet per second + +-- Distance Units: +-- FT = Feet +-- M = Meters +-- MI = Miles +Config.Units = { + Speed = "KTS", -- The speed of the helicopter (unit type: speed) + Altitude = "FT", -- The altitude of the helicopter (unit type: distance) + TargetSpeed = "MPH", -- The speed of the target the camera is looking at (unit type: speed) + TargetElevation = "FT", -- The elevation of where the camera is aming (unit type: distance) + TargetDistance = "M" -- The distance from the helicopter to the target (unit type: distance) +} + +Config.Camera = { + MovementSpeed = { + Keyboard = 3.0, + Controller = 1.0 + }, + Zoom = { + Max = 50.0, + Min = 5.0, + Speed = 5.0 + }, + RotationLimits = { + Up = 25.0, + Down = -89.5 + } +} + +-- Docs: https://docs.fivem.net/docs/game-references/input-mapper-parameter-ids/ -- https://docs.fivem.net/docs/game-references/input-mapper-parameter-ids/keyboard/ +Config.Keybinds = { + ToggleCam = { + Type = "KEYBOARD", + Key = "E" + }, + AttemptLock = { + Type = "KEYBOARD", + Key = "SPACE" + }, + CycleVision = { + Type = "MOUSE_BUTTON", + Key = "MOUSE_RIGHT" + }, + ToggleMarker = { + Type = "MOUSE_BUTTON", + Key = "MOUSE_MIDDLE" + }, + Rappel = { + Type = "KEYBOARD", + Key = "X" + }, + Spotlight = { + Type = "KEYBOARD", + Key = "G" + }, + SpotlightBrightness = { + Type = "KEYBOARD", + Key = "LMENU" + }, + SpotlightRadius = { + Type = "KEYBOARD", + Key = "LCONTROL" + }, + Postals = { + Type = "KEYBOARD", + Key = "" + } +} + +Config.Localisation = { + -- Notifications + Notification = { + JobNotWhitelisted = "You don't have the required job!", + JobGrade = "Your job grade is to low!", + NoCameraHeli = "This helicopter doesn't have a camera!", + NoCameraPlane = "This plane doesn't have a camera!", + IsPilot = "You can't use the camera while you are the pilot!", + NotInRear = "You need to be in the rear of the helicopter to use the camera!", + CameraInUse = "Someone else is already using the camera!", + SpotlightInUse = "Someone else is already using the camera spotlight!", + NoSpotlight = "This helicopter does not have a spotlight!", + SpotlightGlobalLimit = "The global spotlight limit has been reached!", + CannotRappelFromHeli = "This helicopter does not support rappelling!", + CannotRappelFromSeat = "You cannot rappel from this seat!", + ToHighToRappel = "The helicopter is to far up to rappel!", + ConfirmRappel = "Are you sure you want to rappel? (Press again to confirm)", + Rappelling = "Rappelling!" + }, + -- Blip Names + Blip = { + Target = "Helicam Target", + Marker = "Helicam Marker" + }, + -- Keybinding Descriptions + KeyMapping = { + ToggleCam = "Helicam - Toggle Camera", + AttemptLock = "Helicam - Attempt Lock", + CycleVision = "Helicam - Cycle Vision", + ToggleMarker = "Helicam - Add/Remove Markers", + Rappel = "Helicam - Rappel From Helicopter", + Spotlight = "Helicam - Toggle Spotlight", + SpotlightBrightness = "Helicam - (+ scroll) Adjust Spotlight Brightness", + SpotlightRadius = "Helicam - (+ scroll) Adjust Spotlight Radius", + Postals = "Helicam - Toggle Postals" + }, + ChatSuggestions = { + ToggleCamera = "Enter/Exit the helicopter camera", + Rappel = "Rappel from the helicopter" + } +} + +-- Nightvision/Thermal/Spotlight/PassengerOnly etc. can be manually enabled/disabled (overwriten) for each model by adding one or more of following varabels under the model. +-- nightvision = true / false +-- thermalvision = true / false +-- spotlight = true / false +-- passengerOnly = false / 1 / 2 (see Config.PassengerOnly) +-- disableRappelling = true / false +-- timecycle = "modiferName" / false +-- timecycleStrength = 0.0 to 1.0 +Config.Helicopters = { + -- Default (if there is any missing data it will draw it's options from here) + default = { + offset = vector3(0.0, 0.0, -1.0), + -- nightvision = true, -- These are only needed/used if Config.AllowNightVision or Config.AllowThermal is set to false + -- thermalvision = true, -- You can add these to each and every model, adding these and setting them to false disables the vision even if Config.AllowThermal etc. is set to true + -- spotlight = true, -- Allows yo overwrite Config.AllowSpotlight on a helicopter to helicopter basis. + -- passengerOnly = false, -- Set's who can use the camera based on the seat they are in. (see Config.PassengerOnly) + -- disableRappelling = false, -- Disables rappelling for the helicopter model, will only make a diffrence when set to true. The helicopter also needs the "FLAG_ALLOWS_RAPPEL" flag too allow you to rappel out of it. + -- timecycle = "CAMERA_BW", -- The timecycle modifer name (can be set to false if you want to disable it) + -- timecycleStrength = 0.5, -- The strength of the timecycle, defaults to Config.DefaultCameraTimecycleStrength if not included. + labels = { + [0] = "FLIR SYSTEMS" + } + }, + -- Police Maverick (polmav) + [353883353] = { + offset = vector3(0.0, 2.65, -1.0), + labels = { -- Liveries + [0] = "LOS SANTOS POLICE DEPARTMENT", -- 0 is default + [1] = "AIR AMBULANCE" + } + }, + -- Maverick (maverick) + [-1660661558] = { + offset = vector3(0.0, 3.45, -0.65) + }, + -- Buzzard Attack Chopper (buzzard) + [788747387] = { + offset = vector3(0.0, 2.15, -0.35) + }, + -- Buzzard (buzzard2) + [745926877] = { + offset = vector3(0.0, 2.15, -0.35) + }, + -- Frogger (frogger) + [744705981] = { + offset = vector3(0.0, 3.0, -0.35) + }, + -- TPI/FIB Frogger (frogger2) + [1949211328] = { + offset = vector3(0.0, 3.0, -0.35), + labels = { -- Liveries + [0] = "FEDERAL INVESTIGATION BUREAU", + [1] = "TREVOR PHILIPS ENTERPRISES" + } + }, + -- Annihilator/Patriotism and Immigration Authority (annihilator) + [837858166] = { + offset = vector3(-0.5, 4.0, -0.35), + labels = { -- Liveries + [0] = "NATIONAL OFFICE OF SECURITY ENFORCMENT" + } + }, + -- Valkyrie (valkyrie) + [-1600252419] = { + offset = vector3(0.0, 4.0, -1.15), + labels = { -- Liveries + [0] = "UNITED STATES ARMY" + } + }, + -- Avenger (avenger) + [-2118308144] = { + offset = vector3(0.0, 9.45, -2.45), + labels = { -- Liveries + [0] = "UNITED STATES MARINES" + } + }, + + -- Example of custom helicopter: + -- Emergency Maverick AS350 (eheli) + -- [`eheli`] = { + -- offset = vector3(0.0, 3.0, -1.15), + -- labels = { -- Liveries + -- [0] = "LOS SANTOS POLICE DEPARTMENT", + -- [1] = "SAN ANDREAS HIGHWAY PATROL", + -- [2] = "BLAIN COUNTY SHERIFF OFFICE", + -- [3] = "SAN ANDREAS FIRE DEPARTMENT" + -- } + -- }, + -- You can also do GetHashKey("eheli") instead of `eheli` + -- Here is a guide if you need more help: https://madsl.gitbook.io/docs/resources/helicopter-camera/adding-custom-helicopters +} diff --git a/resources/helicam/framework/esx.lua b/resources/helicam/framework/esx.lua new file mode 100644 index 000000000..f25589637 --- /dev/null +++ b/resources/helicam/framework/esx.lua @@ -0,0 +1,20 @@ +if GetResourceState('es_extended') ~= 'started' then return end + +ESX = exports.es_extended:getSharedObject() + +function JobCheck() + if Config.WhitelistedJobs == false then + return true, nil + end + + local PlayerData = ESX.GetPlayerData() + if Config.WhitelistedJobs[PlayerData.job.name] then + if PlayerData.job.grade >= Config.WhitelistedJobs[PlayerData.job.name] then + return true, nil + else + return false, (Config.ShowMessageIfWrongJob and 'JobGrade') or nil + end + else + return false, (Config.ShowMessageIfWrongJob and 'JobNotWhitelisted') or nil + end +end diff --git a/resources/helicam/framework/qb.lua b/resources/helicam/framework/qb.lua new file mode 100644 index 000000000..7005e07b0 --- /dev/null +++ b/resources/helicam/framework/qb.lua @@ -0,0 +1,20 @@ +if GetResourceState('qb-core') ~= 'started' then return end + +QBCore = exports['qb-core']:GetCoreObject() + +function JobCheck() + if Config.WhitelistedJobs == false then + return true, nil + end + + local PlayerData = QBCore.Functions.GetPlayerData() + if Config.WhitelistedJobs[PlayerData.job.name] then + if PlayerData.job.grade.level >= Config.WhitelistedJobs[PlayerData.job.name] then + return true, nil + else + return false, (Config.ShowMessageIfWrongJob and 'JobGrade') or nil + end + else + return false, (Config.ShowMessageIfWrongJob and 'JobNotWhitelisted') or nil + end +end diff --git a/resources/helicam/framework/standalone.lua b/resources/helicam/framework/standalone.lua new file mode 100644 index 000000000..e1adb4c23 --- /dev/null +++ b/resources/helicam/framework/standalone.lua @@ -0,0 +1,9 @@ +if GetResourceState('es_extended') == 'started' or GetResourceState('qb-core') == 'started' then return end + +function JobCheck() + if Config.WhitelistedJobs ~= false then + print(GetCurrentResourceName().."/frameworks/standalone.lua: Jobs check failed, none of the supported frameworks are running, please set Config.WhitelistedJobs to false or make code adjustments.") + end + + return true, nil +end diff --git a/resources/helicam/fxmanifest.lua b/resources/helicam/fxmanifest.lua new file mode 100644 index 000000000..b0c13c597 --- /dev/null +++ b/resources/helicam/fxmanifest.lua @@ -0,0 +1,44 @@ +fx_version 'cerulean' +game 'gta5' +lua54 'yes' + +author 'Mads' +description 'Helicam' +version '1.0.9' + +client_scripts { + 'config.lua', + 'numberplates.lua', + 'framework/standalone.lua', + 'framework/esx.lua', + 'framework/qb.lua', + 'client.lua' +} + +server_script 'server.lua' + +escrow_ignore { + 'config.lua', + 'numberplates.lua', + 'framework/standalone.lua', + 'framework/esx.lua', + 'framework/qb.lua', + 'client.lua', + 'server.lua' +} + +ui_page('html/index.html') + +files { + 'html/index.html', + 'html/script.js', + 'html/style.css', + 'html/images/*.svg' +} + +dependencies { + '/server:5181', + '/gameBuild:2060' -- Needed due to usage of game events. +} + +dependency '/assetpacks' \ No newline at end of file diff --git a/resources/helicam/html/images/arrow.svg b/resources/helicam/html/images/arrow.svg new file mode 100644 index 000000000..0e5e07702 --- /dev/null +++ b/resources/helicam/html/images/arrow.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/resources/helicam/html/images/bearing-directions.svg b/resources/helicam/html/images/bearing-directions.svg new file mode 100644 index 000000000..35d247e97 --- /dev/null +++ b/resources/helicam/html/images/bearing-directions.svg @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/resources/helicam/html/images/bearing.svg b/resources/helicam/html/images/bearing.svg new file mode 100644 index 000000000..852f0fa24 --- /dev/null +++ b/resources/helicam/html/images/bearing.svg @@ -0,0 +1,63 @@ + \ No newline at end of file diff --git a/resources/helicam/html/images/camera-heading.svg b/resources/helicam/html/images/camera-heading.svg new file mode 100644 index 000000000..aa8e7d088 --- /dev/null +++ b/resources/helicam/html/images/camera-heading.svg @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/resources/helicam/html/images/camera-pitch.svg b/resources/helicam/html/images/camera-pitch.svg new file mode 100644 index 000000000..3d491b0b2 --- /dev/null +++ b/resources/helicam/html/images/camera-pitch.svg @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/resources/helicam/html/images/line.svg b/resources/helicam/html/images/line.svg new file mode 100644 index 000000000..9368c21ff --- /dev/null +++ b/resources/helicam/html/images/line.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/resources/helicam/html/images/satellite-icon.svg b/resources/helicam/html/images/satellite-icon.svg new file mode 100644 index 000000000..fbd445026 --- /dev/null +++ b/resources/helicam/html/images/satellite-icon.svg @@ -0,0 +1,11 @@ + diff --git a/resources/helicam/html/images/target-cross-inner.svg b/resources/helicam/html/images/target-cross-inner.svg new file mode 100644 index 000000000..cba2eafd2 --- /dev/null +++ b/resources/helicam/html/images/target-cross-inner.svg @@ -0,0 +1,5 @@ + + diff --git a/resources/helicam/html/images/target-cross.svg b/resources/helicam/html/images/target-cross.svg new file mode 100644 index 000000000..ebbeeb576 --- /dev/null +++ b/resources/helicam/html/images/target-cross.svg @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/resources/helicam/html/images/zoom-bar.svg b/resources/helicam/html/images/zoom-bar.svg new file mode 100644 index 000000000..b46d4a5e2 --- /dev/null +++ b/resources/helicam/html/images/zoom-bar.svg @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/resources/helicam/html/index.html b/resources/helicam/html/index.html new file mode 100644 index 000000000..fb3ded3da --- /dev/null +++ b/resources/helicam/html/index.html @@ -0,0 +1,144 @@ + + +
+ + + +FLIR SYSTEMS
+None
.
+SPD
0
ALT
0
+KTS
+FT
+.
+HDG
0
+°T
+Made By Mads
None
.
+SPD
0
ELV
0
+MPH
+FT
+.
+HDG
0
SLT
0
+°T
+M
+LPL
---
0°T
+21/08/22
00:00:00 Z
Scanning...
+HDEO
+LOCK
NONE
+W
+ +N
+0°
+0°
+