From b623ac866c24a6c938ace226486cfa00a22865a6 Mon Sep 17 00:00:00 2001 From: Artur Michalak <36608116+ArturMichalak@users.noreply.github.com> Date: Fri, 19 Apr 2024 17:35:36 +0200 Subject: [PATCH] refactor: Re-organization and rename * merge main to pr * fix: qb-vehiclekeys:client:giveKeys to qb-vehiclekeys:client:GiveKeys in event register * Refactor: Putting the lockpicking in a separate thread has made the UI smoother --- client/functions.lua | 167 +++++++++++- client/main.lua | 585 ++++++++++++++++++------------------------- fxmanifest.lua | 39 +-- server/commands.lua | 2 +- server/functions.lua | 10 +- server/main.lua | 15 +- server/version.lua | 1 + shared/functions.lua | 23 +- 8 files changed, 468 insertions(+), 374 deletions(-) create mode 100644 server/version.lua diff --git a/client/functions.lua b/client/functions.lua index 225f926..635ec94 100644 --- a/client/functions.lua +++ b/client/functions.lua @@ -1,6 +1,13 @@ +local config = require 'config.client' +local functions = require 'shared.functions' +local getHash, isCloseToCoords in functions + +local alertSend = false +local public = {} + --- Checks if the current player has a key for the specified vehicle. ---@param vehicle number The entity number of the vehicle to check for a key. ----@return boolean | nil if the player has a key for the vehicle, nil otherwise. +---@return boolean? `true` if the player has a key for the vehicle, nil otherwise. function HasKey(vehicle) if not vehicle or type(vehicle) ~= 'number' then return end local ent = Entity(vehicle) @@ -31,3 +38,161 @@ function ToggleVehicleDoor(vehicle) -- Will call the corresponding callback end +--- Checking weapon on the blacklist. +--- @return boolean? `true` if the vehicle is blacklisted, nil otherwise. +function public.isBlacklistedWeapon() + local weapon = GetSelectedPedWeapon(cache.ped) + + for _, v in pairs(config.noCarjackWeapons) do + if weapon == getHash(v) then return true end + end +end + +--- Checking vehicle on the blacklist. +--- @param vehicle number The entity number of the vehicle. +--- @return boolean? `true` if the vehicle is blacklisted, nil otherwise. +function public.isBlacklistedVehicle(vehicle) + if Entity(vehicle).state.ignoreLocks or GetVehicleClass(vehicle) == 13 then return true end + + local vehicleHash = GetEntityModel(vehicle) + for _, v in ipairs(config.noLockVehicles) do + if vehicleHash == getHash(v) then return true end + end +end + +function public.attemptPoliceAlert(type) + if not alertSend then + local chance = config.policeAlertChance + if GetClockHours() >= 1 and GetClockHours() <= 6 then + chance = config.policeNightAlertChance + end + if math.random() <= chance then + TriggerServerEvent('police:server:policeAlert', locale("info.vehicle_theft") .. type) + end + alertSend = true + SetTimeout(config.alertCooldown, function() + alertSend = false + end) + end +end + +--- Gets bone coords +--- @param vehicle number The entity number of the vehicle. +--- @param boneName string The entity bone name. +--- @return vector3 Bone coords if exists, entity coords otherwise. +local function getBoneCoords(vehicle, boneName) + local boneIndex = GetEntityBoneIndexByName(vehicle, boneName) + + if boneIndex ~= -1 then + return GetWorldPositionOfEntityBone(vehicle, boneIndex) + else + return GetEntityCoords(vehicle) + end +end + +--- Checking whether the character is close enough to the vehicle driver door. +--- @param vehicle number The entity number of the vehicle. +--- @param maxDistance number The max distance to check. +--- @return boolean? `true` if the ped is out of a vehicle and in the range of the opened vehicle, nil otherwise. +local function isVehicleInRange(vehicle, maxDistance) + local vehicles = GetGamePool('CVehicle') + local pedCoords = GetEntityCoords(cache.ped) + + for _, v in ipairs(vehicles) do + if not cache.vehicle or v ~= cache.vehicle then + if vehicle == v then + local doorCoords = getBoneCoords(vehicle, 'door_dside_f') + if isCloseToCoords(doorCoords, pedCoords, maxDistance) then return true end + end + end + end +end + +--- The function will be execuded when the opening of the lock succeeds. +--- @param vehicle number The entity number of the vehicle. +--- @param plate string The plate number of the vehicle. +local function lockpickSuccessCallback(vehicle, plate) + TriggerServerEvent('hud:server:GainStress', math.random(1, 4)) + + if cache.seat == -1 then + TriggerServerEvent('qb-vehiclekeys:server:AcquireVehicleKeys', plate) + else + exports.qbx_core:Notify(locale("notify.vehicle_lockedpick"), 'success') + TriggerServerEvent('qb-vehiclekeys:server:setVehLockState', NetworkGetNetworkIdFromEntity(vehicle), 1) + Entity(vehicle).state.isOpen = true + end +end + +--- Operations done after the LockpickDoor quickevent done. +--- @param vehicle number The entity number of the vehicle. +--- @param plate string The plate number of the vehicle. +--- @param isAdvancedLockedpick boolean Determines whether an advanced lockpick was used. +--- @param maxDistance number The max distance to check. +--- @param isSuccess boolean? Determines whether the lock has been successfully opened. +local function lockpickCallback(vehicle, plate, isAdvancedLockedpick, maxDistance, isSuccess) + if not isVehicleInRange(vehicle, maxDistance) then return end -- the action will be aborted if the opened vehicle is too far. + if isSuccess then + lockpickSuccessCallback(vehicle, plate) + else -- if player fails quickevent + public.attemptPoliceAlert('carjack') + TriggerServerEvent('hud:server:GainStress', math.random(1, 4)) + exports.qbx_core:Notify('You failed to lockpick.', 'error') + end + + local chance = math.random() + if isAdvancedLockedpick then -- there is no benefit to using an advanced tool at this moment. + if chance <= config.removeAdvancedLockpickChance[GetVehicleClass(vehicle)] then + TriggerServerEvent("qb-vehiclekeys:server:breakLockpick", "advancedlockpick") + end + else + if chance <= config.removeNormalLockpickChance[GetVehicleClass(vehicle)] then + TriggerServerEvent("qb-vehiclekeys:server:breakLockpick", "lockpick") + end + end +end + +local lockpickingSemaphore = 0 +--- Lockpicking quickevent. +--- @param isAdvancedLockedpick boolean Determines whether an advanced lockpick was used +function public.lockpickDoor(isAdvancedLockedpick) + local maxDistance = 2 + local pedCoords = GetEntityCoords(cache.ped) + local vehicle = lib.getClosestVehicle(pedCoords, 4, false) + + if not vehicle then return end + + local plate = qbx.getVehiclePlate(vehicle) + local isDriverSeatFree = IsVehicleSeatFree(vehicle, -1) + local doorCoords = getBoneCoords(vehicle, 'door_dside_f') + + --- player may attempt to open the lock if: + if not vehicle + or not plate + or not isDriverSeatFree -- no one in the driver's seat + or Entity(vehicle).state.isOpen -- the lock is locked + or not isCloseToCoords(doorCoords, pedCoords, maxDistance) -- the player's ped is close enough to the driver's door + or GetVehicleDoorLockStatus(vehicle) < 2 -- the vehicle is locked + or lib.callback.await('qbx_vehiclekeys:server:hasKeys', false, plate) -- player does not have keys to the vehicle + then + return + end + + lockpickingSemaphore = lockpickingSemaphore + 1 -- semaphore + if lockpickingSemaphore > 1 then return end + Wait(0) + + CreateThread(function() + --- lock opening animation + lib.requestAnimDict('veh@break_in@0h@p_m_one@') + TaskPlayAnim(cache.ped, 'veh@break_in@0h@p_m_one@', "low_force_entry_ds", 3.0, 3.0, -1, 16, 0, false, false, false) + + local isSuccess = lib.skillCheck({ 'easy', 'easy', { areaSize = 60, speedMultiplier = 1 }, 'medium' }, + { '1', '2', '3', '4' }) + + lockpickCallback(vehicle, plate, isAdvancedLockedpick, maxDistance, isSuccess) + end) + + lockpickingSemaphore = 0 +end + +return public diff --git a/client/main.lua b/client/main.lua index 7247fc3..2279fa1 100644 --- a/client/main.lua +++ b/client/main.lua @@ -1,238 +1,26 @@ +----------------------- +---- Imports ---- +----------------------- + +local config = require 'config.client' +local functions = require 'client.functions' +local lockpickDoor, attemptPoliceAlert, isBlacklistedWeapon, isBlacklistedVehicle in functions + ----------------------- ---- Variables ---- ----------------------- + local KeysList = {} local isTakingKeys = false local isCarjacking = false local canCarjack = true -local alertSend = false -local lastPickedVehicle = nil -local usingAdvanced = false local isHotwiring = false -local config = require 'config.client' - ------------------------ ----- Threads ---- ------------------------ -CreateThread(function() - while true do - local sleep = 1000 - if LocalPlayer.state.isLoggedIn then - sleep = 100 - - - local entering = GetVehiclePedIsTryingToEnter(cache.ped) - local carIsImmune = false - if entering ~= 0 and not isBlacklistedVehicle(entering) then - sleep = 2000 - local plate = qbx.getVehiclePlate(entering) - - local driver = GetPedInVehicleSeat(entering, -1) - for _, veh in ipairs(config.immuneVehicles) do - if GetEntityModel(entering) == joaat(veh) then - carIsImmune = true - end - end - -- Driven vehicle logic - if driver ~= 0 and not IsPedAPlayer(driver) and not HasKeys(plate) and not carIsImmune then - if IsEntityDead(driver) then - if not isTakingKeys then - isTakingKeys = true - - TriggerServerEvent('qb-vehiclekeys:server:setVehLockState', NetworkGetNetworkIdFromEntity(entering), 1) - if lib.progressCircle({ - duration = 2500, - label = locale("progress.takekeys"), - position = 'bottom', - useWhileDead = false, - canCancel = true, - disable = { - car = true, - }, - }) then - TriggerServerEvent('qb-vehiclekeys:server:AcquireVehicleKeys', plate) - isTakingKeys = false - else - isTakingKeys = false - end - end - elseif config.lockNPCDrivenCars then - TriggerServerEvent('qb-vehiclekeys:server:setVehLockState', NetworkGetNetworkIdFromEntity(entering), 2) - else - TriggerServerEvent('qb-vehiclekeys:server:setVehLockState', NetworkGetNetworkIdFromEntity(entering), 1) - TriggerServerEvent('qb-vehiclekeys:server:AcquireVehicleKeys', plate) - - --Make passengers flee - local pedsInVehicle = GetPedsInVehicle(entering) - for _, pedInVehicle in pairs(pedsInVehicle) do - if pedInVehicle ~= GetPedInVehicleSeat(entering, -1) then - MakePedFlee(pedInVehicle) - end - end - end - -- Parked car logic - elseif driver == 0 and entering ~= lastPickedVehicle and not HasKeys(plate) and not isTakingKeys then - if config.lockNPCParkedCars then - TriggerServerEvent('qb-vehiclekeys:server:setVehLockState', NetworkGetNetworkIdFromEntity(entering), 2) - else - TriggerServerEvent('qb-vehiclekeys:server:setVehLockState', NetworkGetNetworkIdFromEntity(entering), 1) - end - end - end - - -- Hotwiring while in vehicle, also keeps engine off for vehicles you don't own keys to - if cache.vehicle and not isHotwiring then - sleep = 1000 - local plate = qbx.getVehiclePlate(cache.vehicle) - - if GetPedInVehicleSeat(cache.vehicle, -1) == cache.ped and not HasKeys(plate) and not isBlacklistedVehicle(cache.vehicle) and not AreKeysJobShared(cache.vehicle) then - sleep = 0 - - local vehiclePos = GetOffsetFromEntityInWorldCoords(cache.vehicle, 0.0, 1.0, 0.5) - qbx.drawText3d({ text = locale('info.search_keys'), coords = vehiclePos }) - SetVehicleEngineOn(cache.vehicle, false, false, true) - - if IsControlJustPressed(0, 74) then - Hotwire(cache.vehicle, plate) - end - end - end - - if config.carjackEnable and canCarjack then - local aiming, target = GetEntityPlayerIsFreeAimingAt(cache.playerId) - if aiming and (target ~= nil and target ~= 0) then - if DoesEntityExist(target) and IsPedInAnyVehicle(target, false) and not IsEntityDead(target) and not IsPedAPlayer(target) then - local targetveh = GetVehiclePedIsIn(target, false) - for _, veh in ipairs(config.immuneVehicles) do - if GetEntityModel(targetveh) == joaat(veh) then - carIsImmune = true - end - end - if GetPedInVehicleSeat(targetveh, -1) == target and not IsBlacklistedWeapon() then - local pos = GetEntityCoords(cache.ped, true) - local targetpos = GetEntityCoords(target, true) - if #(pos - targetpos) < 5.0 and not carIsImmune then - CarjackVehicle(target) - end - end - end - end - end - end - Wait(sleep) - end -end) - -function isBlacklistedVehicle(vehicle) - local isBlacklisted = false - for _,v in ipairs(config.noLockVehicles) do - if joaat(v) == GetEntityModel(vehicle) then - isBlacklisted = true - break; - end - end - if Entity(vehicle).state.ignoreLocks or GetVehicleClass(vehicle) == 13 then isBlacklisted = true end - return isBlacklisted -end - ------------------------ ----- Client Events ---- ------------------------ - -RegisterKeyMapping('togglelocks', locale("info.toggle_locks"), 'keyboard', 'L') -RegisterCommand('togglelocks', function() - SetVehicleDoorLock(GetVehicle(), nil, true) -end) - -RegisterKeyMapping('engine', locale("info.engine"), 'keyboard', 'G') -RegisterCommand('engine', function() - TriggerEvent("qb-vehiclekeys:client:ToggleEngine") -end) - -AddEventHandler('onResourceStart', function(resourceName) - if resourceName == cache.resource and QBX.PlayerData ~= {} then - GetKeys() - end -end) - --- Handles state right when the player selects their character and location. -RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function() - GetKeys() -end) - --- Resets state on logout, in case of character change. -RegisterNetEvent('QBCore:Client:OnPlayerUnload', function() - KeysList = {} -end) - -RegisterNetEvent('qb-vehiclekeys:client:AddKeys', function(plate) - KeysList[plate] = true - - if cache.vehicle then - local vehicleplate = qbx.getVehiclePlate(cache.vehicle) - - if plate == vehicleplate then - SetVehicleEngineOn(cache.vehicle, false, false, false) - end - end -end) - -RegisterNetEvent('qb-vehiclekeys:client:RemoveKeys', function(plate) - KeysList[plate] = nil -end) - -RegisterNetEvent('qb-vehiclekeys:client:ToggleEngine', function() - local vehicle = cache.vehicle - if vehicle and HasKeys(qbx.getVehiclePlate(vehicle)) then - local engineOn = GetIsVehicleEngineRunning(vehicle) - if engineOn then - SetVehicleEngineOn(vehicle, false, false, true) - else - SetVehicleEngineOn(vehicle, true, false, true) - end - end -end) - -RegisterNetEvent('qb-vehiclekeys:client:GiveKeys', function(id) - local targetVehicle = GetVehicle() - - if targetVehicle then - local targetPlate = qbx.getVehiclePlate(targetVehicle) - if HasKeys(targetPlate) then - if id and type(id) == "number" then -- Give keys to specific ID - GiveKeys(id, targetPlate) - else - if IsPedSittingInVehicle(cache.ped, targetVehicle) then -- Give keys to everyone in vehicle - local otherOccupants = GetOtherPlayersInVehicle(targetVehicle) - for p = 1, #otherOccupants do - TriggerServerEvent('qb-vehiclekeys:server:GiveVehicleKeys', GetPlayerServerId(NetworkGetPlayerIndexFromPed(otherOccupants[p])), targetPlate) - end - else -- Give keys to closest player - local playerId, _, _ = lib.getClosestPlayer(GetEntityCoords(cache.ped), 3, false) - GiveKeys(playerId, targetPlate) - end - end - else - exports.qbx_core:Notify(locale("notify.no_keys"), 'error') - end - end -end) - -RegisterNetEvent('lockpicks:UseLockpick', function(isAdvanced) - LockpickDoor(isAdvanced) -end) - --- Backwards Compatibility ONLY -- Remove at some point -- -RegisterNetEvent('vehiclekeys:client:SetOwner', function(plate) - TriggerServerEvent('qb-vehiclekeys:server:AcquireVehicleKeys', plate) -end) --- Backwards Compatibility ONLY -- Remove at some point -- ----------------------- ---- Functions ---- ----------------------- -function GiveKeys(id, plate) +local function giveKeys(id, plate) local distance = #(GetEntityCoords(cache.ped) - GetEntityCoords(GetPlayerPed(GetPlayerFromServerId(id)))) if distance < 1.5 and distance > 0.0 then TriggerServerEvent('qb-vehiclekeys:server:GiveVehicleKeys', id, plate) @@ -241,28 +29,30 @@ function GiveKeys(id, plate) end end -function GetKeys() +local function getKeys() lib.callback('qbx-vehiclekeys:server:getVehicleKeys', false, function(keysList) - KeysList = keysList + KeysList = keysList end) end -function HasKeys(plate) +local function hasKeys(plate) return KeysList[plate] end -exports('HasKeys', HasKeys) +exports('HasKeys', hasKeys) -function GetVehicleInDirection(coordFromOffset, coordToOffset) - local coordFrom = GetOffsetFromEntityInWorldCoords(cache.ped, coordFromOffset.x, coordFromOffset.y, coordFromOffset.z) +local function getVehicleInDirection(coordFromOffset, coordToOffset) + local coordFrom = GetOffsetFromEntityInWorldCoords(cache.ped, coordFromOffset.x, coordFromOffset.y, coordFromOffset + .z) local coordTo = GetOffsetFromEntityInWorldCoords(cache.ped, coordToOffset.x, coordToOffset.y, coordToOffset.z) - local rayHandle = CastRayPointToPoint(coordFrom.x, coordFrom.y, coordFrom.z, coordTo.x, coordTo.y, coordTo.z, 10, cache.ped, 0) + local rayHandle = CastRayPointToPoint(coordFrom.x, coordFrom.y, coordFrom.z, coordTo.x, coordTo.y, coordTo.z, 10, + cache.ped, 0) local _, _, _, _, vehicle = GetShapeTestResult(rayHandle) return vehicle end -- If in vehicle returns that, otherwise tries 3 different raycasts to get the vehicle they are facing. -- Raycasts picture: https://i.imgur.com/FRED0kV.png -function GetVehicle() +local function getVehicle() local vehicle = cache.vehicle local RaycastOffsetTable = { @@ -274,14 +64,14 @@ function GetVehicle() local count = 0 while not vehicle and count < #RaycastOffsetTable do count = count + 1 - vehicle = GetVehicleInDirection(RaycastOffsetTable[count]['fromOffset'], RaycastOffsetTable[count]['toOffset']) + vehicle = getVehicleInDirection(RaycastOffsetTable[count]['fromOffset'], RaycastOffsetTable[count]['toOffset']) end if not IsEntityAVehicle(vehicle) then vehicle = nil end return vehicle end -function AreKeysJobShared(veh) +local function areKeysJobShared(veh) local vehName = GetDisplayNameFromVehicleModel(GetEntityModel(veh)) local vehPlate = GetVehicleNumberPlateText(veh) for job, v in pairs(config.sharedKeys) do @@ -289,7 +79,7 @@ function AreKeysJobShared(veh) if config.sharedKeys[job].requireOnduty and not QBX.PlayerData.job.onduty then return false end for _, vehicle in pairs(v.vehicles) do if string.upper(vehicle) == string.upper(vehName) then - if not HasKeys(vehPlate) then + if not hasKeys(vehPlate) then TriggerServerEvent("qb-vehiclekeys:server:AcquireVehicleKeys", vehPlate) end return true @@ -300,10 +90,10 @@ function AreKeysJobShared(veh) return false end -function SetVehicleDoorLock(veh, state, anim) +local function setVehicleDoorLock(veh, state, anim) if veh then if not isBlacklistedVehicle(veh) then - if HasKeys(qbx.getVehiclePlate(veh)) or AreKeysJobShared(veh) then + if hasKeys(qbx.getVehiclePlate(veh)) or areKeysJobShared(veh) then local vehLockStatus = GetVehicleDoorLockStatus(veh) if anim then @@ -342,9 +132,9 @@ function SetVehicleDoorLock(veh, state, anim) end end end -exports("SetVehicleDoorLock", SetVehicleDoorLock) +exports("SetVehicleDoorLock", setVehicleDoorLock) -function GetOtherPlayersInVehicle(vehicle) +local function getOtherPlayersInVehicle(vehicle) local otherPeds = {} for seat = -1, GetVehicleModelNumberOfSeats(GetEntityModel(vehicle)) - 2 do local pedInSeat = GetPedInVehicleSeat(vehicle, seat) @@ -355,7 +145,7 @@ function GetOtherPlayersInVehicle(vehicle) return otherPeds end -function GetPedsInVehicle(vehicle) +local function getPedsInVehicle(vehicle) local otherPeds = {} for seat = -1, GetVehicleModelNumberOfSeats(GetEntityModel(vehicle)) - 2 do local pedInSeat = GetPedInVehicleSeat(vehicle, seat) @@ -366,84 +156,12 @@ function GetPedsInVehicle(vehicle) return otherPeds end -function IsBlacklistedWeapon() - local weapon = GetSelectedPedWeapon(cache.ped) - if weapon ~= nil then - for _, v in pairs(config.noCarjackWeapons) do - if weapon == joaat(v) then - return true - end - end - end - return false -end - -function LockpickDoor(isAdvanced) - local pos = GetEntityCoords(cache.ped) - local vehicle = lib.getClosestVehicle(pos, 2, false) - - if not vehicle then return end - if HasKeys(qbx.getVehiclePlate(vehicle)) then return end - if #(pos - GetEntityCoords(vehicle)) > 2.5 then return end - if GetVehicleDoorLockStatus(vehicle) <= 0 then return end - - SetVehicleAlarm(vehicle, true) - SetVehicleAlarmTimeLeft(vehicle, 2) - - usingAdvanced = isAdvanced - lib.requestAnimDict('veh@break_in@0h@p_m_one@') - TaskPlayAnim(cache.ped, 'veh@break_in@0h@p_m_one@', "low_force_entry_ds", 3.0, 3.0, -1, 16, 0, false, false, false) - local success = lib.skillCheck({'easy', 'easy', {areaSize = 60, speedMultiplier = 1}, 'medium'}, {'1', '2', '3', '4'}) - SetTimeout(10000, function() - if not DoesEntityExist(vehicle) then return end - if not IsEntityAVehicle(vehicle) then return end - if isCarjacking then return end - if IsVehicleAlarmActivated(vehicle) then - SetVehicleAlarm(vehicle, false) - end - end) - if success then - LockpickFinishCallback(success) - else - AttemptPoliceAlert('carjack') - TriggerServerEvent('hud:server:GainStress', math.random(1, 4)) - TriggerEvent('QBCore:Notify', 'You failed to lockpick.', 'error') - end - end - -function LockpickFinishCallback(success) - local vehicle = lib.getClosestVehicle(GetEntityCoords(cache.ped), 2, true) - if not vehicle then return end - - local chance = math.random() - if success then - TriggerServerEvent('hud:server:GainStress', math.random(1, 4)) - lastPickedVehicle = vehicle - - if cache.seat == -1 then - TriggerServerEvent('qb-vehiclekeys:server:AcquireVehicleKeys', qbx.getVehiclePlate(vehicle)) - else - exports.qbx_core:Notify(locale("notify.vehicle_lockedpick"), 'success') - TriggerServerEvent('qb-vehiclekeys:server:setVehLockState', NetworkGetNetworkIdFromEntity(vehicle), 1) - end - - else - TriggerServerEvent('hud:server:GainStress', math.random(1, 4)) - AttemptPoliceAlert("steal") - end - - if usingAdvanced then - if chance <= config.removeAdvancedLockpickChance[GetVehicleClass(vehicle)] then - TriggerServerEvent("qb-vehiclekeys:server:breakLockpick", "advancedlockpick") - end - else - if chance <= config.removeNormalLockpickChance[GetVehicleClass(vehicle)] then - TriggerServerEvent("qb-vehiclekeys:server:breakLockpick", "lockpick") - end - end +local function makePedFlee(ped) + SetPedFleeAttributes(ped, 0, false) + TaskReactAndFleePed(ped, cache.ped) end -function Hotwire(vehicle, plate) +local function hotwire(vehicle, plate) local hotwireTime = math.random(config.minHotwireTime, config.maxHotwireTime) isHotwiring = true SetVehicleAlarm(vehicle, true) @@ -487,25 +205,25 @@ function Hotwire(vehicle, plate) isHotwiring = false end SetTimeout(10000, function() - AttemptPoliceAlert("steal") + attemptPoliceAlert("steal") end) isHotwiring = false end -function CarjackVehicle(target) +local function carjackVehicle(target) if not config.carjackEnable then return end isCarjacking = true canCarjack = false lib.requestAnimDict('mp_am_hold_up') local vehicle = GetVehiclePedIsUsing(target) - local occupants = GetPedsInVehicle(vehicle) + local occupants = getPedsInVehicle(vehicle) for p = 1, #occupants do local ped = occupants[p] CreateThread(function() TaskPlayAnim(ped, "mp_am_hold_up", "holdup_victim_20s", 8.0, -8.0, -1, 49, 0, false, false, false) PlayPain(ped, 6, 0) end) - Wait(math.random(200,500)) + Wait(math.random(200, 500)) end -- Cancel progress bar if: Ped dies during robbery, car gets too far away CreateThread(function() @@ -538,55 +256,240 @@ function CarjackVehicle(target) end if math.random() <= carjackChance then local plate = qbx.getVehiclePlate(vehicle) - for p=1,#occupants do - local ped = occupants[p] - CreateThread(function() + for p = 1, #occupants do + local ped = occupants[p] + CreateThread(function() TaskLeaveVehicle(ped, vehicle, 0) PlayPain(ped, 6, 0) Wait(1250) ClearPedTasksImmediately(ped) PlayPain(ped, math.random(7, 8), 0) - MakePedFlee(ped) + makePedFlee(ped) end) end TriggerServerEvent('hud:server:GainStress', math.random(1, 4)) TriggerServerEvent('qb-vehiclekeys:server:AcquireVehicleKeys', plate) else exports.qbx_core:Notify(locale("notify.carjack_failed"), 'error') - MakePedFlee(target) + makePedFlee(target) TriggerServerEvent('hud:server:GainStress', math.random(1, 4)) end isCarjacking = false Wait(2000) - AttemptPoliceAlert("carjack") + attemptPoliceAlert("carjack") Wait(config.delayBetweenCarjackingsInMs) canCarjack = true end else - MakePedFlee(target) + makePedFlee(target) isCarjacking = false Wait(config.delayBetweenCarjackingsInMs) canCarjack = true end end -function AttemptPoliceAlert(type) - if not alertSend then - local chance = config.policeAlertChance - if GetClockHours() >= 1 and GetClockHours() <= 6 then - chance = config.policeNightAlertChance +----------------------- +---- Threads ---- +----------------------- + +CreateThread(function() + while true do + local sleep = 1000 + if LocalPlayer.state.isLoggedIn then + sleep = 100 + + + local entering = GetVehiclePedIsTryingToEnter(cache.ped) + local carIsImmune = false + if entering ~= 0 and not isBlacklistedVehicle(entering) then + sleep = 2000 + local plate = qbx.getVehiclePlate(entering) + + local driver = GetPedInVehicleSeat(entering, -1) + for _, veh in ipairs(config.immuneVehicles) do + if GetEntityModel(entering) == joaat(veh) then + carIsImmune = true + end + end + -- Driven vehicle logic + if driver ~= 0 and not IsPedAPlayer(driver) and not hasKeys(plate) and not carIsImmune then + if IsEntityDead(driver) then + if not isTakingKeys then + isTakingKeys = true + + TriggerServerEvent('qb-vehiclekeys:server:setVehLockState', NetworkGetNetworkIdFromEntity(entering), 1) + if lib.progressCircle({ + duration = 2500, + label = locale("progress.takekeys"), + position = 'bottom', + useWhileDead = false, + canCancel = true, + disable = { + car = true, + }, + }) then + TriggerServerEvent('qb-vehiclekeys:server:AcquireVehicleKeys', plate) + isTakingKeys = false + else + isTakingKeys = false + end + end + elseif config.lockNPCDrivenCars then + TriggerServerEvent('qb-vehiclekeys:server:setVehLockState', NetworkGetNetworkIdFromEntity(entering), 2) + else + TriggerServerEvent('qb-vehiclekeys:server:setVehLockState', NetworkGetNetworkIdFromEntity(entering), 1) + TriggerServerEvent('qb-vehiclekeys:server:AcquireVehicleKeys', plate) + + --Make passengers flee + local pedsInVehicle = getPedsInVehicle(entering) + for _, pedInVehicle in pairs(pedsInVehicle) do + if pedInVehicle ~= GetPedInVehicleSeat(entering, -1) then + makePedFlee(pedInVehicle) + end + end + end + -- Parked car logic + elseif driver == 0 and not Entity(entering).state.isOpen and not hasKeys(plate) and not isTakingKeys then + if config.lockNPCParkedCars then + TriggerServerEvent('qb-vehiclekeys:server:setVehLockState', NetworkGetNetworkIdFromEntity(entering), 2) + else + TriggerServerEvent('qb-vehiclekeys:server:setVehLockState', NetworkGetNetworkIdFromEntity(entering), 1) + end + end + end + + -- Hotwiring while in vehicle, also keeps engine off for vehicles you don't own keys to + if cache.vehicle and not isHotwiring then + sleep = 1000 + local plate = qbx.getVehiclePlate(cache.vehicle) + + if GetPedInVehicleSeat(cache.vehicle, -1) == cache.ped and not hasKeys(plate) and not isBlacklistedVehicle(cache.vehicle) and not areKeysJobShared(cache.vehicle) then + sleep = 0 + + local vehiclePos = GetOffsetFromEntityInWorldCoords(cache.vehicle, 0.0, 1.0, 0.5) + qbx.drawText3d({ text = locale('info.search_keys'), coords = vehiclePos }) + SetVehicleEngineOn(cache.vehicle, false, false, true) + + if IsControlJustPressed(0, 74) then + hotwire(cache.vehicle, plate) + end + end + end + + if config.carjackEnable and canCarjack then + local aiming, target = GetEntityPlayerIsFreeAimingAt(cache.playerId) + if aiming and (target ~= nil and target ~= 0) then + if DoesEntityExist(target) and IsPedInAnyVehicle(target, false) and not IsEntityDead(target) and not IsPedAPlayer(target) then + local targetveh = GetVehiclePedIsIn(target, false) + for _, veh in ipairs(config.immuneVehicles) do + if GetEntityModel(targetveh) == joaat(veh) then + carIsImmune = true + end + end + if GetPedInVehicleSeat(targetveh, -1) == target and not isBlacklistedWeapon() then + local pos = GetEntityCoords(cache.ped, true) + local targetpos = GetEntityCoords(target, true) + if #(pos - targetpos) < 5.0 and not carIsImmune then + carjackVehicle(target) + end + end + end + end + end end - if math.random() <= chance then - TriggerServerEvent('police:server:policeAlert', locale("info.vehicle_theft") .. type) + Wait(sleep) + end +end) + +----------------------- +---- Client Events ---- +----------------------- + +RegisterKeyMapping('togglelocks', locale("info.toggle_locks"), 'keyboard', 'L') +RegisterCommand('togglelocks', function() + setVehicleDoorLock(getVehicle(), nil, true) +end, false) + +RegisterKeyMapping('engine', locale("info.engine"), 'keyboard', 'G') +RegisterCommand('engine', function() + TriggerEvent("qb-vehiclekeys:client:ToggleEngine") +end, false) + +AddEventHandler('onResourceStart', function(resourceName) + if resourceName == cache.resource and QBX.PlayerData ~= {} then + getKeys() + end +end) + +-- Handles state right when the player selects their character and location. +RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function() + getKeys() +end) + +-- Resets state on logout, in case of character change. +RegisterNetEvent('QBCore:Client:OnPlayerUnload', function() + KeysList = {} +end) + +RegisterNetEvent('qb-vehiclekeys:client:AddKeys', function(plate) + KeysList[plate] = true + + if cache.vehicle then + local vehicleplate = qbx.getVehiclePlate(cache.vehicle) + + if plate == vehicleplate then + SetVehicleEngineOn(cache.vehicle, false, false, false) end - alertSend = true - SetTimeout(config.alertCooldown, function() - alertSend = false - end) end -end +end) -function MakePedFlee(ped) - SetPedFleeAttributes(ped, 0, false) - TaskReactAndFleePed(ped, cache.ped) -end +RegisterNetEvent('qb-vehiclekeys:client:RemoveKeys', function(plate) + KeysList[plate] = nil +end) + +RegisterNetEvent('qb-vehiclekeys:client:ToggleEngine', function() + local vehicle = cache.vehicle + if vehicle and hasKeys(qbx.getVehiclePlate(vehicle)) then + local engineOn = GetIsVehicleEngineRunning(vehicle) + if engineOn then + SetVehicleEngineOn(vehicle, false, false, true) + else + SetVehicleEngineOn(vehicle, true, false, true) + end + end +end) + +RegisterNetEvent('qb-vehiclekeys:client:GiveKeys', function(id) + local targetVehicle = getVehicle() + + if targetVehicle then + local targetPlate = qbx.getVehiclePlate(targetVehicle) + if hasKeys(targetPlate) then + if id and type(id) == "number" then -- Give keys to specific ID + giveKeys(id, targetPlate) + else + if IsPedSittingInVehicle(cache.ped, targetVehicle) then -- Give keys to everyone in vehicle + local otherOccupants = getOtherPlayersInVehicle(targetVehicle) + for p = 1, #otherOccupants do + TriggerServerEvent('qb-vehiclekeys:server:GiveVehicleKeys', GetPlayerServerId(NetworkGetPlayerIndexFromPed(otherOccupants[p])), targetPlate) + end + else -- Give keys to closest player + local playerId, _, _ = lib.getClosestPlayer(GetEntityCoords(cache.ped), 3, false) + giveKeys(playerId, targetPlate) + end + end + else + exports.qbx_core:Notify(locale("notify.no_keys"), 'error') + end + end +end) + +RegisterNetEvent('lockpicks:UseLockpick', function(isAdvanced) + lockpickDoor(isAdvanced) +end) + +-- Backwards Compatibility ONLY -- Remove at some point -- +RegisterNetEvent('vehiclekeys:client:SetOwner', function(plate) + TriggerServerEvent('qb-vehiclekeys:server:AcquireVehicleKeys', plate) +end) +-- Backwards Compatibility ONLY -- Remove at some point -- diff --git a/fxmanifest.lua b/fxmanifest.lua index b8a3693..2ba7709 100644 --- a/fxmanifest.lua +++ b/fxmanifest.lua @@ -1,34 +1,37 @@ +--[[ FX Information ]]-- fx_version 'cerulean' +use_experimental_fxv2_oal 'yes' +lua54 'yes' game 'gta5' -description 'QBX_VehicleKeys' -repository 'https://github.com/Qbox-project/qbx_vehiclekeys' +--[[ Resource Information ]]-- version '1.0.0' +license 'GPL-3.0-or-later' +description 'vehicle key management system' +repository 'https://github.com/Qbox-project/qbx_vehiclekeys' + +--[[ Manifest ]]-- +dependencies { + 'ox_lib', + 'qbx_core' +} ox_lib 'locale' +files { + 'locales/*.json', + 'config/client.lua' +} + shared_scripts { '@ox_lib/init.lua', '@qbx_core/modules/lib.lua', - 'shared/functions.lua' + 'shared/*.lua' } client_scripts { '@qbx_core/modules/playerdata.lua', - 'client/main.lua', - 'client/functions.lua' + 'client/*.lua' } -files { - 'locales/*.json', - 'config/client.lua' -} - -server_scripts { - 'server/main.lua', - 'server/commands.lua', - 'server/functions.lua' -} - -lua54 'yes' -use_experimental_fxv2_oal 'yes' \ No newline at end of file +server_script 'server/*.lua' diff --git a/server/commands.lua b/server/commands.lua index dea62b2..943b2dd 100644 --- a/server/commands.lua +++ b/server/commands.lua @@ -64,4 +64,4 @@ lib.addCommand('removekeys', { return end RemoveKeys(args.id, args.plate) -end) \ No newline at end of file +end) diff --git a/server/functions.lua b/server/functions.lua index 74a2735..fe00663 100644 --- a/server/functions.lua +++ b/server/functions.lua @@ -1,7 +1,7 @@ --- Checks for the existence of a key. ---@param entity number The entity (vehicle) where we check for the existence of a key. ---@param citizenid string The CitizenID of the player whose key we check for. ----@return boolean | nil if the player has a key for the vehicle, nil otherwise. +---@return boolean? `true` if the player has a key for the vehicle, nil otherwise. function HasKey(entity, citizenid) if not entity or type(entity) ~= 'number' or not citizenid or type(citizenid) ~= 'string' then return end local ent = Entity(entity) @@ -13,7 +13,7 @@ end ---@param entity number The entity (vehicle) to which the key is added. ---@param citizenid string The CitizenID of the player whose key is being added. ---@param doorState number | nil -- Sets the doorState of the vehicle if present ----@return boolean | nil `true` if the key was successfully added, `nil` otherwise. +---@return boolean? `true` if the key was successfully added, nil otherwise. function GiveKey(entity, citizenid, doorState) if not entity or type(entity) ~= 'number' or not citizenid or type(citizenid) ~= 'string' then return end @@ -36,7 +36,7 @@ end --- Removes a key from the selected vehicle entity and returns a success status. ---@param entity number The entity (vehicle) from which the key is removed. ---@param citizenid string The CitizenID of the player whose key is being removed. ----@return boolean | nil `true` if the key was successfully removed, `nil` otherwise. +---@return boolean? `true` if the key was successfully removed, `nil` otherwise. function RemoveKey(entity, citizenid) if not entity or type(entity) ~= 'number' or not citizenid or type(citizenid) ~= 'string' then return @@ -56,7 +56,7 @@ end --- Sets the door state of the vehicle. ---@param entity number The entity (vehicle) for which the door state is updated. ---@param doorState number The door state number to update. ----@return boolean | nil `true` if the door state was successfully updated, `nil` otherwise. +---@return boolean? `true` if the door state was successfully updated, `nil` otherwise. function SetDoorState(entity, doorState) if not entity or type(entity) ~= 'number' or not doorState or type(doorState) ~= 'number' then return end @@ -82,4 +82,4 @@ function ToggleDoorState(entity) ent.state:set('doorState', 0, true) return 0 end -end \ No newline at end of file +end diff --git a/server/main.lua b/server/main.lua index 710c499..cb11ddc 100644 --- a/server/main.lua +++ b/server/main.lua @@ -11,6 +11,15 @@ local vehicleList = {} ---- Server Events ---- ----------------------- +--- Checking if the player has the vehicle keys +--- @param source number ID of the player +--- @param plate string the vehicle plate value +--- @return boolean? `true` if the player has the vehicle keys, nil otherwise. +lib.callback.register('qbx_vehiclekeys:server:hasKeys', function(source, plate) + local citizenid = exports.qbx_core:GetPlayer(source).PlayerData.citizenid + if vehicleList[plate] and vehicleList[plate][citizenid] then return true end +end) + -- Event to give keys. receiver can either be a single id, or a table of ids. -- Must already have keys to the vehicle, trigger the event from the server, or pass forcegive paramter as true. RegisterNetEvent('qb-vehiclekeys:server:GiveVehicleKeys', function(receiver, plate) @@ -131,7 +140,7 @@ exports('SetDoorState', SetDoorState) ---@param source number ID of the player ---@param netId number The network ID of the entity. ---@param targetPlayerId number ID of the target player who receives the key ----@return boolean | nil +---@return boolean? lib.callback.register('vehiclekeys:server:GiveKey', function(source, netId, targetPlayerId) if not source or not netId or not targetPlayerId then return end -- This callback is not yet implemented @@ -141,7 +150,7 @@ end) ---@param source number ID of the player ---@param netId number The network ID of the entity. ---@param targetPlayerId number ID of the target player who receives the key ----@return boolean | nil +---@return boolean? lib.callback.register('vehiclekeys:server:RemoveKey', function(source, netId, targetPlayerId) if not source or not netId or not targetPlayerId then return end -- This callback is not yet implemented @@ -154,4 +163,4 @@ end) lib.callback.register('vehiclekeys:server:ToggleDoorState', function(source, netId) if not source or not netId then return end -- This callback is not yet implemented -end) \ No newline at end of file +end) diff --git a/server/version.lua b/server/version.lua new file mode 100644 index 0000000..eed7d61 --- /dev/null +++ b/server/version.lua @@ -0,0 +1 @@ +lib.versionCheck('Qbox-project/qbx_vehiclekeys') diff --git a/shared/functions.lua b/shared/functions.lua index 7d7b159..1c9879f 100644 --- a/shared/functions.lua +++ b/shared/functions.lua @@ -1,7 +1,20 @@ +local public = {} + +--- Gets model hash +--- @param model string | number model name or number. +--- @return number hash of the model. +function public.getHash(model) + if type(model) == 'string' then return joaat(model) end + return model +end + --- Checks if the given two coordinates are close to each other based on distance. ----@param coord1 vector3[] The first set of coordinates. ----@param coord2 vector3[] The second set of coordinates. ----@param distance number The maximum allowed distance for them to be considered close. -function IsCloseToCoords(coord1, coord2, distance) +--- @param coord1 vector3[] The first set of coordinates. +--- @param coord2 vector3[] The second set of coordinates. +--- @param distance number The maximum allowed distance for them to be considered close. +--- @return boolean true if the distance between two entities is less than the distance parameter. +function public.isCloseToCoords(coord1, coord2, distance) return #(coord1 - coord2) < distance -end \ No newline at end of file +end + +return public