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)