421 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			421 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
| local QBCore = exports['qb-core']:GetCoreObject()
 | |
| local config = {}
 | |
| 
 | |
| -- Try to load config, with fallback values
 | |
| local success, result = pcall(function() return require('config') end)
 | |
| if success then
 | |
|     config = result
 | |
| else
 | |
|     -- Default config values if loading fails
 | |
|     config = {
 | |
|         trackerItem = 'vehicletracker',
 | |
|         trackerTabletItem = 'vehicletrackertablet',
 | |
|         trackerScannerItem = 'vehicletrackerscanner',
 | |
|         policeJobs = {'police', 'sheriff'}
 | |
|     }
 | |
| end
 | |
| 
 | |
| -- Ensure all config values exist
 | |
| if not config.policeJobs then
 | |
|     config.policeJobs = {'police', 'sheriff'}
 | |
| end
 | |
| 
 | |
| if not config.skillChecks then
 | |
|     config.skillChecks = {
 | |
|         normalLocateOwner = {'easy', 'medium', 'medium'},
 | |
|         policeLocateOwner = {'easy', 'easy', 'medium'},
 | |
|         policeGetPhone = {'medium', 'medium', 'hard'}
 | |
|     }
 | |
| end
 | |
| 
 | |
| if not config.durations then
 | |
|     config.durations = {
 | |
|         ownerBlipDuration = 60000 -- 60 seconds = 1 minute
 | |
|     }
 | |
| end
 | |
| 
 | |
| local trackedVehicles = {}
 | |
| 
 | |
| lib.locale()
 | |
| 
 | |
| -- Functions
 | |
| local function uiNotify(description, nType)
 | |
|     lib.notify({description = description, type = nType, position = 'center-right', iconAnimation = 'bounce', duration = 7000})
 | |
| end
 | |
| 
 | |
| local function uiProgressBar(duration, label, anim, prop)
 | |
|     return lib.progressBar({
 | |
|             duration = duration,
 | |
|             label = label,
 | |
|             useWhileDead = false,
 | |
|             canCancel = true,
 | |
|             disable = { car = true, move = true, combat = true },
 | |
|             anim = anim,
 | |
|             prop = prop
 | |
|     })
 | |
| end
 | |
| 
 | |
| local function playSound(audioName, audioDict)
 | |
|     local soundId = GetSoundId()
 | |
|     PlaySoundFrontend(soundId, audioName, audioDict, false)
 | |
|     SetTimeout(3000, function()
 | |
|         StopSound(soundId)
 | |
|         ReleaseSoundId(soundId)
 | |
|     end)
 | |
| end
 | |
| 
 | |
| local function promptTrackerName(serialNumber, currentName)
 | |
|     local input = lib.inputDialog(locale('vt_name_tracker') or 'Name Your Tracker', {
 | |
|         {
 | |
|             type = 'input',
 | |
|             label = locale('vt_tracker_name') or 'Tracker Name',
 | |
|             description = locale('vt_name_description') or 'Enter a name to help identify this tracker',
 | |
|             default = currentName or '',
 | |
|             required = true
 | |
|         }
 | |
|     })
 | |
|     
 | |
|     if not input or input[1] == '' then return false end
 | |
|     
 | |
|     -- Update the tracker name on the server
 | |
|     lib.callback('qb_vehicle_tracker:updateTrackerName', false, function(success)
 | |
|         if success then
 | |
|             uiNotify(locale('vt_name_updated') or 'Tracker name updated', 'success')
 | |
|         else
 | |
|             uiNotify(locale('vt_name_failed') or 'Failed to update tracker name', 'error')
 | |
|         end
 | |
|     end, serialNumber, input[1])
 | |
|     
 | |
|     return true
 | |
| end
 | |
| 
 | |
| -- Function to check if player is police
 | |
| local function isPlayerPolice()
 | |
|     local PlayerData = QBCore.Functions.GetPlayerData()
 | |
|     if not PlayerData or not PlayerData.job then return false end
 | |
|     
 | |
|     for _, jobName in pairs(config.policeJobs) do
 | |
|         if PlayerData.job.name == jobName then
 | |
|             return true
 | |
|         end
 | |
|     end
 | |
|     
 | |
|     return false
 | |
| end
 | |
| 
 | |
| -- Events
 | |
| RegisterNetEvent('qb_vehicle_tracker:client:showTrackerMenu', function(citizenid)
 | |
|     lib.callback('qb_vehicle_tracker:getPlayerTrackers', false, function(trackers)
 | |
|         if not trackers or #trackers == 0 then 
 | |
|             uiNotify(locale('vt_no_trackers_found') or "No trackers found", 'error')
 | |
|             return 
 | |
|         end
 | |
|         
 | |
|         local options = {}
 | |
|         for _, tracker in ipairs(trackers) do
 | |
|             local displayName = tracker.name or tracker.vehiclePlate
 | |
|             
 | |
|             table.insert(options, {
 | |
|                 title = displayName,
 | |
|                 description = (locale('vt_vehicle_plate') or "Vehicle Plate") .. ': ' .. tracker.vehiclePlate,
 | |
|                 metadata = {
 | |
|                     {label = locale('vt_serial_number') or "Serial Number", value = tracker.serialNumber}
 | |
|                 },
 | |
|                 icon = 'car',
 | |
|                 onSelect = function()
 | |
|                     -- Show submenu for this tracker
 | |
|                     lib.registerContext({
 | |
|                         id = 'tracker_options_' .. tracker.serialNumber,
 | |
|                         title = displayName,
 | |
|                         menu = 'vt_menu',
 | |
|                         options = {
 | |
|                             {
 | |
|                                 title = locale('vt_locate_tracker') or "Locate Vehicle",
 | |
|                                 description = locale('vt_locate_description') or "Show the vehicle's location on the map",
 | |
|                                 icon = 'location-dot',
 | |
|                                 onSelect = function()
 | |
|                                     TriggerEvent('qb_vehicle_tracker:client:locateTracker', tracker.serialNumber)
 | |
|                                 end
 | |
|                             },
 | |
|                             {
 | |
|                                 title = locale('vt_rename_tracker') or "Rename Tracker",
 | |
|                                 description = locale('vt_rename_description') or "Change the name of this tracker",
 | |
|                                 icon = 'pen',
 | |
|                                 onSelect = function()
 | |
|                                     promptTrackerName(tracker.serialNumber, tracker.name)
 | |
|                                     -- Refresh the menu after renaming
 | |
|                                     Wait(500)
 | |
|                                     TriggerEvent('qb_vehicle_tracker:client:showTrackerMenu', citizenid)
 | |
|                                 end
 | |
|                             }
 | |
|                         }
 | |
|                     })
 | |
|                     lib.showContext('tracker_options_' .. tracker.serialNumber)
 | |
|                 end
 | |
|             })
 | |
|         end
 | |
|         
 | |
|         lib.registerContext({
 | |
|             id = 'vt_menu',
 | |
|             title = locale('vt_menu_header') or "Vehicle Tracker",
 | |
|             options = options
 | |
|         })
 | |
|         
 | |
|         if uiProgressBar(2000, locale('vt_pb_connecting') or "Connecting to tracker network...", {
 | |
|             dict = 'amb@code_human_in_bus_passenger_idles@female@tablet@base',
 | |
|             clip = 'base'
 | |
|         }, {
 | |
|             model = `prop_cs_tablet`,
 | |
|             pos = vec3(0.03, 0.002, -0.0),
 | |
|             rot = vec3(10.0, 160.0, 0.0)
 | |
|         }) then 
 | |
|             lib.showContext('vt_menu') 
 | |
|         else 
 | |
|             uiNotify(locale('vt_pb_cancelled') or "Cancelled", 'error') 
 | |
|         end
 | |
|     end, citizenid)
 | |
| end)
 | |
| 
 | |
| RegisterNetEvent('qb_vehicle_tracker:client:scanTracker', function(slot)
 | |
|     local vehicle = lib.getClosestVehicle(GetEntityCoords(cache.ped), 3.0, true)
 | |
|     if vehicle == nil or not DoesEntityExist(vehicle) then uiNotify(locale('vt_no_vehicle_nearby'), 'error') return end
 | |
| 
 | |
|     if uiProgressBar(6000, locale('vt_pb_scanning'), {
 | |
|         dict = 'anim@amb@clubhouse@tutorial@bkr_tut_ig3@',
 | |
|         clip = 'machinic_loop_mechandplayer',
 | |
|         flag = 1
 | |
|     }, {
 | |
|         model = `w_am_digiscanner`,
 | |
|         pos = vec3(0.06, 0.03, -0.1),
 | |
|         rot = vec3(10.0, 190.0, 0.0)
 | |
|     }) then
 | |
|         lib.callback('qb_vehicle_tracker:isVehicleTracked', false, function(veh)
 | |
|             if veh == nil then uiNotify(locale('vt_no_tracker'), 'info') return end
 | |
| 
 | |
|             playSound('TIMER_STOP', 'HUD_MINI_GAME_SOUNDSET')
 | |
| 
 | |
|             -- Create scanner options menu
 | |
|             local options = {
 | |
|                 {
 | |
|                     title = locale('vt_remove_tracker') or "Remove Tracker",
 | |
|                     description = locale('vt_remove_description') or "Remove the tracking device from this vehicle",
 | |
|                     icon = 'trash',
 | |
|                     onSelect = function()
 | |
|                         TriggerEvent('qb_vehicle_tracker:client:removeTracker', slot)
 | |
|                     end
 | |
|                 },
 | |
|                 {
 | |
|                     title = locale('vt_locate_owner') or "Locate Tracker Owner",
 | |
|                     description = locale('vt_locate_owner_description') or "Try to trace the signal back to the owner",
 | |
|                     icon = 'satellite-dish',
 | |
|                     onSelect = function()
 | |
|                         TriggerEvent('qb_vehicle_tracker:client:locateTrackerOwner', GetVehicleNumberPlateText(vehicle))
 | |
|                     end
 | |
|                 }
 | |
|             }
 | |
|             
 | |
|             -- Add police-only option to get phone number
 | |
|             if isPlayerPolice() then
 | |
|                 table.insert(options, {
 | |
|                     title = locale('vt_get_phone') or "Get Owner's Phone Number",
 | |
|                     description = locale('vt_get_phone_description') or "Try to extract the owner's phone number",
 | |
|                     icon = 'phone',
 | |
|                     onSelect = function()
 | |
|                         TriggerEvent('qb_vehicle_tracker:client:getTrackerOwnerPhone', GetVehicleNumberPlateText(vehicle))
 | |
|                     end
 | |
|                 })
 | |
|             end
 | |
|             
 | |
|             lib.registerContext({
 | |
|                 id = 'scanner_menu',
 | |
|                 title = locale('vt_scanner_menu_header') or "Tracker Scanner",
 | |
|                 options = options
 | |
|             })
 | |
|             
 | |
|             lib.showContext('scanner_menu')
 | |
|         end, GetVehicleNumberPlateText(vehicle))
 | |
|     else
 | |
|         uiNotify(locale('vt_pb_cancelled'), 'error')
 | |
|     end
 | |
| end)
 | |
| 
 | |
| RegisterNetEvent('qb_vehicle_tracker:client:placeTracker', function(slot, serialNumber)
 | |
|     local vehicle = lib.getClosestVehicle(GetEntityCoords(cache.ped), 2.5, true)
 | |
|     if vehicle == nil or not DoesEntityExist(vehicle) then uiNotify(locale('vt_no_vehicle_nearby'), 'error') return end
 | |
| 
 | |
|     if uiProgressBar(6000, locale('vt_pb_placing'), {
 | |
|         dict = 'anim@amb@clubhouse@tutorial@bkr_tut_ig3@',
 | |
|         clip = 'machinic_loop_mechandplayer',
 | |
|         flag = 1
 | |
|     }, {
 | |
|         model = `prop_prototype_minibomb`,
 | |
|         pos = vec3(0.1, 0.03, -0.0),
 | |
|         rot = vec3(10.0, 160.0, 0.0)
 | |
|     }) then
 | |
|         lib.callback('qb_vehicle_tracker:placeTracker', false, function(success)
 | |
|             if not success then return end
 | |
|             playSound('Hack_Success', 'DLC_HEIST_BIOLAB_PREP_HACKING_SOUNDS')
 | |
|             uiNotify(locale('vt_placed_success'), 'success')
 | |
|         end, GetVehicleNumberPlateText(vehicle), slot, serialNumber)
 | |
|     else
 | |
|         uiNotify(locale('vt_pb_cancelled'), 'error')
 | |
|     end
 | |
| end)
 | |
| 
 | |
| RegisterNetEvent('qb_vehicle_tracker:client:removeTracker', function(slot)
 | |
|     local vehicle = lib.getClosestVehicle(GetEntityCoords(cache.ped), 3.0, true)
 | |
|     if vehicle == nil or not DoesEntityExist(vehicle) then uiNotify(locale('vt_no_vehicle_nearby'), 'error') return end
 | |
| 
 | |
|     local vehPlate = GetVehicleNumberPlateText(vehicle)
 | |
| 
 | |
|     lib.callback('qb_vehicle_tracker:isVehicleTracked', false, function(veh)
 | |
|         if veh == nil then return uiNotify(locale('vt_no_tracker'), 'info') end
 | |
|         if uiProgressBar(6000, locale('vt_pb_removing'), {
 | |
|             dict = 'anim@amb@clubhouse@tutorial@bkr_tut_ig3@',
 | |
|             clip = 'machinic_loop_mechandplayer',
 | |
|             flag = 1
 | |
|         }, {}) then
 | |
|             lib.callback('qb_vehicle_tracker:removeTracker', false, function(success)
 | |
|                 if not success then return end
 | |
| 
 | |
|                 if trackedVehicles[veh.serialNumber] then
 | |
|                     RemoveBlip(trackedVehicles[veh.serialNumber])
 | |
| 
 | |
|                     trackedVehicles[veh.serialNumber] = nil
 | |
|                 end
 | |
| 
 | |
|                 playSound('Hack_Success', 'DLC_HEIST_BIOLAB_PREP_HACKING_SOUNDS')
 | |
|                 uiNotify(locale('vt_remove_success'), 'success')
 | |
|             end, vehPlate, slot)
 | |
|         else
 | |
|             uiNotify(locale('vt_pb_cancelled'), 'error')
 | |
|         end
 | |
|     end, vehPlate)
 | |
| end)
 | |
| 
 | |
| RegisterNetEvent('qb_vehicle_tracker:client:locateTracker', function(serialNumber)
 | |
|     if serialNumber == nil then uiNotify(locale('vt_not_placed'), 'error') return end
 | |
| 
 | |
|     lib.callback('qb_vehicle_tracker:getTrackedVehicleBySerial', false, function(veh, vehCoords, trackerName, isLastKnown)
 | |
|         if veh == nil then uiNotify(locale('vt_unable_connect'), 'error') return end
 | |
| 
 | |
|         local blip = AddBlipForCoord(vehCoords.x, vehCoords.y, 0.0)
 | |
| 
 | |
|         SetBlipSprite(blip, 161)
 | |
|         SetBlipColour(blip, isLastKnown and 3 or 1) -- Use yellow for last known position, red for current
 | |
|         SetBlipAlpha(blip, 250)
 | |
|         SetBlipDisplay(blip, 2)
 | |
|         SetBlipScale(blip, 2.5)
 | |
|         PulseBlip(blip)
 | |
|         SetBlipAsShortRange(blip, false)
 | |
|         BeginTextCommandSetBlipName('STRING')
 | |
|         
 | |
|         local blipName = trackerName or ('Tracker ' .. veh)
 | |
|         if isLastKnown then
 | |
|             blipName = blipName .. " (Last Known)"
 | |
|         end
 | |
|         
 | |
|         AddTextComponentSubstringPlayerName(blipName)
 | |
|         EndTextCommandSetBlipName(blip)
 | |
|         SetNewWaypoint(vehCoords.x, vehCoords.y)
 | |
| 
 | |
|         trackedVehicles[serialNumber] = blip
 | |
| 
 | |
|         playSound('10_SEC_WARNING', 'HUD_MINI_GAME_SOUNDSET')
 | |
|         
 | |
|         if isLastKnown then
 | |
|             uiNotify(locale('vt_last_known_position') or "Showing last known position of vehicle", 'info')
 | |
|         else
 | |
|             uiNotify(locale('vt_connection_success'), 'success')
 | |
|         end
 | |
|     end, serialNumber)
 | |
| end)
 | |
| 
 | |
| 
 | |
| -- New events for advanced scanner features
 | |
| RegisterNetEvent('qb_vehicle_tracker:client:locateTrackerOwner', function(vehiclePlate)
 | |
|     -- Determine which skill check difficulty to use based on player job
 | |
|     local skillCheckDifficulty = config.skillChecks.normalLocateOwner
 | |
|     if isPlayerPolice() then
 | |
|         skillCheckDifficulty = config.skillChecks.policeLocateOwner
 | |
|     end
 | |
|     
 | |
|     -- Perform a skill check with appropriate difficulty
 | |
|     local success = lib.skillCheck(skillCheckDifficulty, {'w', 'a', 's', 'd'})
 | |
|     
 | |
|     if success then
 | |
|         lib.callback('qb_vehicle_tracker:getTrackerOwnerLocation', false, function(ownerCoords)
 | |
|             if not ownerCoords then
 | |
|                 uiNotify(locale('vt_owner_not_found') or "Could not trace the signal back to the owner", 'error')
 | |
|                 return
 | |
|             end
 | |
|             
 | |
|             local blip = AddBlipForCoord(ownerCoords.x, ownerCoords.y, 0.0)
 | |
|             
 | |
|             SetBlipSprite(blip, 280)
 | |
|             SetBlipColour(blip, 1)
 | |
|             SetBlipAlpha(blip, 250)
 | |
|             SetBlipDisplay(blip, 2)
 | |
|             SetBlipScale(blip, 1.0)
 | |
|             PulseBlip(blip)
 | |
|             SetBlipAsShortRange(blip, false)
 | |
|             BeginTextCommandSetBlipName('STRING')
 | |
|             AddTextComponentSubstringPlayerName(locale('vt_tracker_owner') or "Tracker Owner")
 | |
|             EndTextCommandSetBlipName(blip)
 | |
|             SetNewWaypoint(ownerCoords.x, ownerCoords.y)
 | |
|             
 | |
|             -- Store the blip with a unique key
 | |
|             local uniqueKey = "owner_" .. vehiclePlate
 | |
|             trackedVehicles[uniqueKey] = blip
 | |
|             
 | |
|             playSound('Hack_Success', 'DLC_HEIST_BIOLAB_PREP_HACKING_SOUNDS')
 | |
|             uiNotify(locale('vt_owner_located') or "Successfully traced signal to the owner's location", 'success')
 | |
|             
 | |
|             -- Remove the blip after configured time
 | |
|             SetTimeout(config.durations.ownerBlipDuration, function()
 | |
|                 if trackedVehicles[uniqueKey] then
 | |
|                     RemoveBlip(trackedVehicles[uniqueKey])
 | |
|                     trackedVehicles[uniqueKey] = nil
 | |
|                 end
 | |
|             end)
 | |
|         end, vehiclePlate)
 | |
|     else
 | |
|         uiNotify(locale('vt_trace_failed') or "Failed to trace the signal", 'error')
 | |
|         playSound('Failure', 'DLC_HEIST_HACKING_SNAKE_SOUNDS')
 | |
|     end
 | |
| end)
 | |
| 
 | |
| RegisterNetEvent('qb_vehicle_tracker:client:getTrackerOwnerPhone', function(vehiclePlate)
 | |
|     -- Perform a more difficult skill check for police using configured difficulty
 | |
|     local success = lib.skillCheck(config.skillChecks.policeGetPhone, {'w', 'a', 's', 'd'})
 | |
|     
 | |
|     if success then
 | |
|         lib.callback('qb_vehicle_tracker:getTrackerOwnerPhone', false, function(phoneNumber)
 | |
|             if not phoneNumber then
 | |
|                 uiNotify(locale('vt_phone_not_found') or "Could not extract phone number from the tracker", 'error')
 | |
|                 return
 | |
|             end
 | |
|             
 | |
|             playSound('Hack_Success', 'DLC_HEIST_BIOLAB_PREP_HACKING_SOUNDS')
 | |
|             uiNotify((locale('vt_phone_found') or "Phone number extracted: ") .. phoneNumber, 'success')
 | |
|         end, vehiclePlate)
 | |
|     else
 | |
|         uiNotify(locale('vt_phone_extraction_failed') or "Failed to extract phone number", 'error')
 | |
|         playSound('Failure', 'DLC_HEIST_HACKING_SNAKE_SOUNDS')
 | |
|     end
 | |
| end)
 | |
| 
 | |
| CreateThread(function()
 | |
|     while true do
 | |
|         Wait(3000)
 | |
|         for serialNumber, blip in pairs(trackedVehicles) do
 | |
|             local blipAlpha = GetBlipAlpha(blip)
 | |
|             if blipAlpha > 0 then
 | |
|                 SetBlipAlpha(blip, blipAlpha - 10)
 | |
|             else
 | |
|                 trackedVehicles[serialNumber] = nil
 | |
|                 RemoveBlip(blip)
 | |
|             end
 | |
|         end
 | |
|     end
 | |
| end)
 | 
