forked from Simnation/Main
2029 lines
69 KiB
Lua
2029 lines
69 KiB
Lua
-- 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)
|