255 lines
11 KiB
Lua
255 lines
11 KiB
Lua
|
|
local config = require 'config.client'
|
||
|
|
|
||
|
|
---Grants keys for job shared vehicles
|
||
|
|
---@param vehicle number The entity number of the vehicle.
|
||
|
|
---@return boolean? `true` if the vehicle is shared for a player's job, `nil` otherwise.
|
||
|
|
function AreKeysJobShared(vehicle)
|
||
|
|
local job = QBX.PlayerData.job.name
|
||
|
|
local jobInfo = config.sharedKeys[job]
|
||
|
|
|
||
|
|
if not jobInfo or (jobInfo.requireOnDuty and not QBX.PlayerData.job.onduty) then return end
|
||
|
|
|
||
|
|
assert(jobInfo.vehicles, string.format('Vehicles not configured for the %s job.', job))
|
||
|
|
return jobInfo.vehicles and jobInfo.vehicles[GetEntityModel(vehicle)] or jobInfo.classes and jobInfo.classes[GetVehicleClass(vehicle)]
|
||
|
|
end
|
||
|
|
|
||
|
|
---Checks if player has vehicle keys
|
||
|
|
---@param vehicle number
|
||
|
|
---@return boolean `true` if player has vehicle keys, `false` otherwise.
|
||
|
|
function HasKeys(vehicle)
|
||
|
|
vehicle = vehicle or cache.vehicle
|
||
|
|
if not vehicle then return false end
|
||
|
|
local keysList = LocalPlayer.state.keysList
|
||
|
|
if keysList then
|
||
|
|
local sessionId = Entity(vehicle).state.sessionId
|
||
|
|
if keysList[sessionId] then
|
||
|
|
return true
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
local owner = Entity(vehicle).state.owner
|
||
|
|
if owner and QBX.PlayerData.citizenid == owner then
|
||
|
|
lib.callback.await('qbx_vehiclekeys:server:giveKeys', false, VehToNet(vehicle))
|
||
|
|
return true
|
||
|
|
end
|
||
|
|
|
||
|
|
return false
|
||
|
|
end
|
||
|
|
|
||
|
|
exports('HasKeys', HasKeys)
|
||
|
|
|
||
|
|
---Checks if player has vehicle keys of or access to the vehicle is provided as part of his job.
|
||
|
|
---@param vehicle number The entity number of the vehicle.
|
||
|
|
---@return boolean? `true` if player has access to the vehicle, `nil` otherwise.
|
||
|
|
function GetIsVehicleAccessible(vehicle)
|
||
|
|
return HasKeys(vehicle) or AreKeysJobShared(vehicle)
|
||
|
|
end
|
||
|
|
|
||
|
|
local alertSend = false --Variable strictly related to sendPoliceAlertAttempt, not used elsewhere
|
||
|
|
function SendPoliceAlertAttempt(crime, vehicle)
|
||
|
|
if alertSend then return end
|
||
|
|
alertSend = true
|
||
|
|
|
||
|
|
local hoursOffset = (24 + GetClockHours() - config.policeAlertNightStartHour) % 24; --Hour from the start of the night hours
|
||
|
|
local chance = hoursOffset > config.policeAlertNightDuration
|
||
|
|
and config.policeAlertChance
|
||
|
|
or config.policeNightAlertChance
|
||
|
|
|
||
|
|
if math.random() <= chance then
|
||
|
|
config.alertPolice(crime, vehicle)
|
||
|
|
end
|
||
|
|
|
||
|
|
SetTimeout(config.alertCooldown, function()
|
||
|
|
alertSend = false
|
||
|
|
end)
|
||
|
|
end
|
||
|
|
|
||
|
|
---Gets bone coords
|
||
|
|
---@param entity number The entity index.
|
||
|
|
---@param boneName string The entity bone name.
|
||
|
|
---@return vector3 `Bone coords` if exists, `entity coords` otherwise.
|
||
|
|
local function getBoneCoords(entity, boneName)
|
||
|
|
local boneIndex = GetEntityBoneIndexByName(entity, boneName)
|
||
|
|
|
||
|
|
return boneIndex ~= -1
|
||
|
|
and GetWorldPositionOfEntityBone(entity, boneIndex)
|
||
|
|
or GetEntityCoords(entity)
|
||
|
|
end
|
||
|
|
|
||
|
|
---Checks if any of the bones are close enough to the coords
|
||
|
|
---@param coords vector3
|
||
|
|
---@param entity number
|
||
|
|
---@param bones table
|
||
|
|
---@param maxDistance number
|
||
|
|
---@return boolean? `true` if bone exists, `nil` otherwise.
|
||
|
|
local function getIsCloseToAnyBone(coords, entity, bones, maxDistance)
|
||
|
|
for i = 1, #bones do
|
||
|
|
local boneCoords = getBoneCoords(entity, bones[i])
|
||
|
|
if #(coords - boneCoords) < maxDistance then
|
||
|
|
return true
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
local doorBones = {'door_dside_f', 'door_dside_r', 'door_pside_f', 'door_pside_r'}
|
||
|
|
|
||
|
|
---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 player ped is next to an open vehicle, `nil` otherwise.
|
||
|
|
local function getIsVehicleInRange(vehicle, maxDistance)
|
||
|
|
local vehicles = GetGamePool('CVehicle')
|
||
|
|
local pedCoords = GetEntityCoords(cache.ped)
|
||
|
|
for i = 1, #vehicles do
|
||
|
|
local v = vehicles[i]
|
||
|
|
if not cache.vehicle or v ~= cache.vehicle then
|
||
|
|
if vehicle == v and getIsCloseToAnyBone(pedCoords, vehicle, doorBones, maxDistance) then
|
||
|
|
return true
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
---Chance to destroy lockpick
|
||
|
|
---@param isAdvancedLockedpick any
|
||
|
|
---@param vehicle number
|
||
|
|
local function breakLockpick(isAdvancedLockedpick, vehicle)
|
||
|
|
local chance = math.random()
|
||
|
|
local vehicleConfig = GetVehicleConfig(vehicle)
|
||
|
|
if isAdvancedLockedpick then -- there is no benefit to using an advanced tool in the default configuration.
|
||
|
|
if chance <= vehicleConfig.removeAdvancedLockpickChance then
|
||
|
|
TriggerServerEvent("qb-vehiclekeys:server:breakLockpick", "advancedlockpick")
|
||
|
|
end
|
||
|
|
else
|
||
|
|
if chance <= vehicleConfig.removeNormalLockpickChance then
|
||
|
|
TriggerServerEvent("qb-vehiclekeys:server:breakLockpick", "lockpick")
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
---Will be executed when the lock opening is successful.
|
||
|
|
---@param vehicle number The entity number of the vehicle.
|
||
|
|
local function lockpickSuccessCallback(vehicle)
|
||
|
|
TriggerServerEvent('hud:server:GainStress', math.random(1, 4))
|
||
|
|
exports.qbx_core:Notify(locale("notify.vehicle_lockedpick"), 'success')
|
||
|
|
TriggerServerEvent('qb-vehiclekeys:server:setVehLockState', NetworkGetNetworkIdFromEntity(vehicle), 1)
|
||
|
|
end
|
||
|
|
|
||
|
|
---Operations done after the LockpickDoor quickevent done.
|
||
|
|
---@param vehicle number The entity number of the vehicle.
|
||
|
|
---@param isAdvancedLockedpick boolean Determines whether an advanced lockpick was used.
|
||
|
|
---@param isSuccess boolean? Determines whether the lock has been successfully opened.
|
||
|
|
local function lockpickCallback(vehicle, isAdvancedLockedpick, isSuccess)
|
||
|
|
if isSuccess then
|
||
|
|
lockpickSuccessCallback(vehicle)
|
||
|
|
else -- if player fails quickevent
|
||
|
|
SendPoliceAlertAttempt('carjack', vehicle)
|
||
|
|
SetVehicleAlarm(vehicle, false)
|
||
|
|
SetVehicleAlarmTimeLeft(vehicle, config.vehicleAlarmDuration)
|
||
|
|
TriggerServerEvent('hud:server:GainStress', math.random(1, 4))
|
||
|
|
exports.qbx_core:Notify(locale('notify.failed_lockedpick'), 'error')
|
||
|
|
end
|
||
|
|
|
||
|
|
breakLockpick(isAdvancedLockedpick, vehicle)
|
||
|
|
end
|
||
|
|
|
||
|
|
local islockpickingProcessLocked = false -- lock flag
|
||
|
|
---Lockpicking quickevent.
|
||
|
|
---@param isAdvancedLockedpick boolean Determines whether an advanced lockpick was used
|
||
|
|
---@param maxDistance number? The max distance to check.
|
||
|
|
---@param customChallenge boolean? lockpick challenge
|
||
|
|
function LockpickDoor(isAdvancedLockedpick, maxDistance, customChallenge)
|
||
|
|
maxDistance = maxDistance or 2
|
||
|
|
local pedCoords = GetEntityCoords(cache.ped)
|
||
|
|
local vehicle = lib.getClosestVehicle(pedCoords, maxDistance * 2, false) -- The difference between the door and the center of the vehicle
|
||
|
|
|
||
|
|
if not vehicle then return end
|
||
|
|
|
||
|
|
local isDriverSeatFree = IsVehicleSeatFree(vehicle, -1)
|
||
|
|
|
||
|
|
if GetVehicleDoorLockStatus(vehicle) < 2 then exports.qbx_core:Notify(locale('notify.vehicle_is_unlocked'), 'error') return end
|
||
|
|
|
||
|
|
--- player may attempt to open the lock if:
|
||
|
|
if not isDriverSeatFree -- no one in the driver's seat
|
||
|
|
or not getIsCloseToAnyBone(pedCoords, vehicle, doorBones, maxDistance) -- the player's ped is close enough to the driver's door
|
||
|
|
or GetVehicleConfig(vehicle).lockpickImmune
|
||
|
|
then return end
|
||
|
|
|
||
|
|
local skillCheckConfig = config.skillCheck[isAdvancedLockedpick and 'advancedLockpick' or 'lockpick']
|
||
|
|
|
||
|
|
skillCheckConfig = skillCheckConfig.model[GetEntityModel(vehicle)]
|
||
|
|
or skillCheckConfig.class[GetVehicleClass(vehicle)]
|
||
|
|
or skillCheckConfig.default
|
||
|
|
if not next(skillCheckConfig) then return end
|
||
|
|
|
||
|
|
if islockpickingProcessLocked then return end -- start of the critical section
|
||
|
|
islockpickingProcessLocked = true -- one call per player at a time
|
||
|
|
|
||
|
|
CreateThread(function()
|
||
|
|
local anim = config.anims.lockpick.model[GetEntityModel(vehicle)]
|
||
|
|
or config.anims.lockpick.class[GetVehicleClass(vehicle)]
|
||
|
|
or config.anims.lockpick.default
|
||
|
|
lib.playAnim(cache.ped, anim.dict, anim.clip, 3.0, 3.0, -1, 16, 0, false, false, false) -- lock opening animation
|
||
|
|
local isSuccess = customChallenge or lib.skillCheck(skillCheckConfig.difficulty, skillCheckConfig.inputs)
|
||
|
|
|
||
|
|
if getIsVehicleInRange(vehicle, maxDistance) then -- the action will be aborted if the opened vehicle is too far.
|
||
|
|
lockpickCallback(vehicle, isAdvancedLockedpick, isSuccess)
|
||
|
|
end
|
||
|
|
|
||
|
|
Wait(config.lockpickCooldown)
|
||
|
|
islockpickingProcessLocked = false -- end of the critical section
|
||
|
|
end)
|
||
|
|
end
|
||
|
|
|
||
|
|
---Will be executed when the lock opening is successful.
|
||
|
|
---@param vehicle number The entity number of the vehicle.
|
||
|
|
local function hotwireSuccessCallback(vehicle)
|
||
|
|
TriggerServerEvent('qbx_vehiclekeys:server:hotwiredVehicle', VehToNet(vehicle))
|
||
|
|
end
|
||
|
|
|
||
|
|
---Operations done after the LockpickDoor quickevent done.
|
||
|
|
---@param vehicle number The entity number of the vehicle.
|
||
|
|
---@param isAdvancedLockedpick boolean Determines whether an advanced lockpick was used.
|
||
|
|
---@param isSuccess boolean? Determines whether the lock has been successfully opened.
|
||
|
|
local function hotwireCallback(vehicle, isAdvancedLockedpick, isSuccess)
|
||
|
|
if isSuccess then
|
||
|
|
hotwireSuccessCallback(vehicle)
|
||
|
|
else -- if player fails quickevent
|
||
|
|
SendPoliceAlertAttempt('carjack', vehicle)
|
||
|
|
TriggerServerEvent('hud:server:GainStress', math.random(1, 4))
|
||
|
|
exports.qbx_core:Notify(locale('notify.failed_lockedpick'), 'error')
|
||
|
|
end
|
||
|
|
|
||
|
|
breakLockpick(isAdvancedLockedpick, vehicle)
|
||
|
|
end
|
||
|
|
|
||
|
|
local isHotwiringProcessLocked = false -- lock flag
|
||
|
|
---Hotwiring with a tool quickevent.
|
||
|
|
---@param vehicle number The entity number of the vehicle.
|
||
|
|
---@param isAdvancedLockedpick boolean Determines whether an advanced lockpick was used
|
||
|
|
---@param customChallenge boolean? lockpick challenge
|
||
|
|
function Hotwire(vehicle, isAdvancedLockedpick, customChallenge)
|
||
|
|
if cache.seat ~= -1 or GetIsVehicleAccessible(vehicle) then return end
|
||
|
|
local skillCheckConfig = config.skillCheck[isAdvancedLockedpick and 'advancedHotwire' or 'hotwire']
|
||
|
|
|
||
|
|
skillCheckConfig = skillCheckConfig.model[GetEntityModel(vehicle)]
|
||
|
|
or skillCheckConfig.class[GetVehicleClass(vehicle)]
|
||
|
|
or skillCheckConfig.default
|
||
|
|
if not next(skillCheckConfig) then return end
|
||
|
|
|
||
|
|
if isHotwiringProcessLocked then return end -- start of the critical section
|
||
|
|
isHotwiringProcessLocked = true -- one call per player at a time
|
||
|
|
|
||
|
|
CreateThread(function()
|
||
|
|
local anim = config.anims.hotwire.model[GetEntityModel(vehicle)]
|
||
|
|
or config.anims.hotwire.class[GetVehicleClass(vehicle)]
|
||
|
|
or config.anims.hotwire.default
|
||
|
|
lib.playAnim(cache.ped, anim.dict, anim.clip, 3.0, 3.0, -1, 16, 0, false, false, false) -- lock opening animation
|
||
|
|
local isSuccess = customChallenge or lib.skillCheck(skillCheckConfig.difficulty, skillCheckConfig.inputs)
|
||
|
|
|
||
|
|
hotwireCallback(vehicle, isAdvancedLockedpick, isSuccess)
|
||
|
|
|
||
|
|
Wait(config.hotwireCooldown)
|
||
|
|
isHotwiringProcessLocked = false -- end of the critical section
|
||
|
|
end)
|
||
|
|
end
|