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)
 |