local QBCore = exports['qb-core']:GetCoreObject() local config = require 'config' -- Database functions local function deleteOldTrackers() return true end local function addTracker(serialNumber, vehiclePlate, owner, name, phoneNumber) return MySQL.prepare.await('INSERT INTO `vehicle_trackers` (`serialNumber`, `vehiclePlate`, `owner`, `name`, `phoneNumber`) VALUES (?, ?, ?, ?, ?)', { serialNumber, vehiclePlate, owner, name, phoneNumber }) end local function deleteTracker(vehiclePlate) return MySQL.prepare.await('DELETE FROM `vehicle_trackers` WHERE `vehiclePlate` = ?', { vehiclePlate }) end local function getTracker(serialNumber) return MySQL.single.await('SELECT `serialNumber`, `vehiclePlate`, `name`, `phoneNumber`, `owner`, `lastKnownX`, `lastKnownY`, `lastKnownZ`, `lastUpdated` FROM `vehicle_trackers` WHERE `serialNumber` = ? LIMIT 1', { serialNumber }) end local function isTracked(vehiclePlate) return MySQL.scalar.await('SELECT `serialNumber` FROM `vehicle_trackers` WHERE `vehiclePlate` = ? LIMIT 1', { vehiclePlate }) end local function getPlayerTrackers(owner) return MySQL.query.await('SELECT `serialNumber`, `vehiclePlate`, `name` FROM `vehicle_trackers` WHERE `owner` = ?', { owner }) end local function updateTrackerName(serialNumber, name) return MySQL.prepare.await('UPDATE `vehicle_trackers` SET `name` = ? WHERE `serialNumber` = ?', { name, serialNumber }) end -- New function to update tracker position local function updateTrackerPosition(serialNumber, coords) return MySQL.prepare.await('UPDATE `vehicle_trackers` SET `lastKnownX` = ?, `lastKnownY` = ?, `lastKnownZ` = ? WHERE `serialNumber` = ?', { coords.x, coords.y, coords.z, serialNumber }) end -- Utility functions local function getRandomSerialNumber() return lib.string.random('...........') end local function trim(plate) return (plate:gsub("^%s*(.-)%s*$", "%1")) end local function getVehicleNetworkIdByPlate(vehiclePlate) local vehicles = GetAllVehicles() for _, vehicle in ipairs(vehicles) do if trim(GetVehicleNumberPlateText(vehicle)) == trim(vehiclePlate) then return NetworkGetNetworkIdFromEntity(vehicle) end end return nil end local function isPlayerNearVehicle(playerCoords, vehiclePlate) local vehicle = lib.getClosestVehicle(playerCoords, 3.0, true) if not vehicle or not DoesEntityExist(vehicle) or GetVehicleNumberPlateText(vehicle) ~= vehiclePlate then return false end return true end local function getPlayerPhoneNumber(citizenid) local player = MySQL.single.await('SELECT phone_number FROM players WHERE citizenid = ?', { citizenid }) if player then return player.phone_number end return nil end -- QB Usable Items QBCore.Functions.CreateUseableItem(config.trackerItem, function(source, item) TriggerClientEvent('qb_vehicle_tracker:client:placeTracker', source, item.slot, getRandomSerialNumber()) end) QBCore.Functions.CreateUseableItem(config.trackerTabletItem, function(source, item) local Player = QBCore.Functions.GetPlayer(source) TriggerClientEvent('qb_vehicle_tracker:client:showTrackerMenu', source, Player.PlayerData.citizenid) end) QBCore.Functions.CreateUseableItem(config.trackerScannerItem, function(source, item) TriggerClientEvent('qb_vehicle_tracker:client:scanTracker', source, item.slot) end) -- Event Handler AddEventHandler('onResourceStart', function(resourceName) if GetCurrentResourceName() == resourceName then -- deleteOldTrackers() -- Comment out to prevent deletion after 7 days end end) -- Callbacks lib.callback.register('qb_vehicle_tracker:getTrackedVehicleBySerial', function(_, serialNumber) if type(serialNumber) ~= "string" or string.len(serialNumber) < 11 then return end local tracker = getTracker(serialNumber) if not tracker then return end -- Try to find the vehicle in the world local vehicleNetworkID = getVehicleNetworkIdByPlate(tracker.vehiclePlate) -- If vehicle is found in the world if vehicleNetworkID then local vehicleEntity = NetworkGetEntityFromNetworkId(vehicleNetworkID) if DoesEntityExist(vehicleEntity) then local vehCoords = GetEntityCoords(vehicleEntity) -- Update the last known position in the database updateTrackerPosition(serialNumber, vehCoords) return tracker.vehiclePlate, vector2(vehCoords.x, vehCoords.y), tracker.name end end -- If vehicle is not found in the world, check if we have a last known position if tracker.lastKnownX and tracker.lastKnownY then -- Return the last known position return tracker.vehiclePlate, vector2(tracker.lastKnownX, tracker.lastKnownY), tracker.name, true -- true indicates this is a last known position end -- No current or last known position available return nil end) lib.callback.register('qb_vehicle_tracker:isVehicleTracked', function(source, vehiclePlate) if type(vehiclePlate) ~= "string" or not isPlayerNearVehicle(GetEntityCoords(GetPlayerPed(source)), vehiclePlate) then return false end return isTracked(trim(vehiclePlate)) end) lib.callback.register('qb_vehicle_tracker:placeTracker', function(source, vehiclePlate, slot, serialNumber) if type(vehiclePlate) ~= "string" or type(serialNumber) ~= "string" or string.len(serialNumber) < 11 then return false end if not isPlayerNearVehicle(GetEntityCoords(GetPlayerPed(source)), vehiclePlate) then return false end local Player = QBCore.Functions.GetPlayer(source) local defaultName = "Tracker " .. trim(vehiclePlate) local phoneNumber = getPlayerPhoneNumber(Player.PlayerData.citizenid) or "Unknown" if not addTracker(serialNumber, trim(vehiclePlate), Player.PlayerData.citizenid, defaultName, phoneNumber) then return false end -- Try to get initial position local vehicle = lib.getClosestVehicle(GetEntityCoords(GetPlayerPed(source)), 3.0, true) if vehicle and DoesEntityExist(vehicle) then local vehCoords = GetEntityCoords(vehicle) updateTrackerPosition(serialNumber, vehCoords) end Player.Functions.RemoveItem(config.trackerItem, 1, slot) TriggerClientEvent('inventory:client:ItemBox', source, QBCore.Shared.Items[config.trackerItem], 'remove') return true end) lib.callback.register('qb_vehicle_tracker:removeTracker', function(source, vehiclePlate, slot) if type(vehiclePlate) ~= "string" or not isPlayerNearVehicle(GetEntityCoords(GetPlayerPed(source)), vehiclePlate) then return false end if not deleteTracker(trim(vehiclePlate)) then return false end local Player = QBCore.Functions.GetPlayer(source) if Player.Functions.RemoveItem(config.trackerScannerItem, 1, slot) then TriggerClientEvent('inventory:client:ItemBox', source, QBCore.Shared.Items[config.trackerScannerItem], 'remove') end return true end) lib.callback.register('qb_vehicle_tracker:getPlayerTrackers', function(source, citizenid) local Player = QBCore.Functions.GetPlayer(source) if Player.PlayerData.citizenid ~= citizenid then return {} end return getPlayerTrackers(citizenid) end) lib.callback.register('qb_vehicle_tracker:updateTrackerName', function(source, serialNumber, name) if type(serialNumber) ~= "string" or string.len(serialNumber) < 11 or type(name) ~= "string" then return false end local Player = QBCore.Functions.GetPlayer(source) -- Get the tracker to verify it exists local tracker = getTracker(serialNumber) if not tracker then return false end -- Verify the player owns trackers with this serial number local playerTrackers = getPlayerTrackers(Player.PlayerData.citizenid) local isOwner = false for _, t in ipairs(playerTrackers) do if t.serialNumber == serialNumber then isOwner = true break end end if not isOwner then return false end -- Update the name return updateTrackerName(serialNumber, name) end) -- Callbacks for advanced scanner features lib.callback.register('qb_vehicle_tracker:getTrackerOwnerLocation', function(source, vehiclePlate) if type(vehiclePlate) ~= "string" then return nil end local serialNumber = isTracked(trim(vehiclePlate)) if not serialNumber then return nil end local tracker = getTracker(serialNumber) if not tracker or not tracker.owner then return nil end local targetPlayer = QBCore.Functions.GetPlayerByCitizenId(tracker.owner) if not targetPlayer then return nil end local targetPed = GetPlayerPed(targetPlayer.PlayerData.source) if not targetPed then return nil end local targetCoords = GetEntityCoords(targetPed) return vector2(targetCoords.x, targetCoords.y) end) lib.callback.register('qb_vehicle_tracker:getTrackerOwnerPhone', function(source, vehiclePlate) local Player = QBCore.Functions.GetPlayer(source) if not Player then return nil end -- Check if player is police local isPolice = false for _, jobName in pairs(config.policeJobs) do if Player.PlayerData.job.name == jobName then isPolice = true break end end if not isPolice then return nil end if type(vehiclePlate) ~= "string" then return nil end local serialNumber = isTracked(trim(vehiclePlate)) if not serialNumber then return nil end local tracker = getTracker(serialNumber) if not tracker or not tracker.phoneNumber then return nil end return tracker.phoneNumber end) -- Add this thread to periodically update positions of all tracked vehicles CreateThread(function() while true do Wait(300000) -- Update every 5 minutes -- Get all tracked vehicles local trackers = MySQL.query.await('SELECT `serialNumber`, `vehiclePlate` FROM `vehicle_trackers`') for _, tracker in ipairs(trackers) do local vehicleNetworkID = getVehicleNetworkIdByPlate(tracker.vehiclePlate) if vehicleNetworkID then local vehicleEntity = NetworkGetEntityFromNetworkId(vehicleNetworkID) if DoesEntityExist(vehicleEntity) then local vehCoords = GetEntityCoords(vehicleEntity) updateTrackerPosition(tracker.serialNumber, vehCoords) end end end end end)