diff --git a/client/vehicle-persistence.lua b/client/vehicle-persistence.lua new file mode 100644 index 000000000..790826a08 --- /dev/null +++ b/client/vehicle-persistence.lua @@ -0,0 +1,74 @@ +if GetConvar('qbx:enableVehiclePersistence', 'false') == 'false' then return end + +local cachedProps +local netId +local vehicle +local seat + +local watchedKeys = { + 'bodyHealth', + 'engineHealth', + 'tankHealth', + 'fuelLevel', + 'oilLevel', + 'dirtLevel', + 'windows', + 'doors', + 'tyres', +} + +---Calculates the difference in values of two tables for the watched keys. +---If the second table does not have a value that the first table has, it will be marked 'deleted'. +---@param tbl1 table +---@param tbl2 table +---@return table diff +---@return boolean hasChanged if diff table is not empty +local function calculateDiff(tbl1, tbl2) + local diff = {} + local hasChanged = false + + for i = 1, #watchedKeys do + local key = watchedKeys[i] + local val1 = tbl1[key] + local val2 = tbl2[key] + + if val1 ~= val2 then + diff[key] = val2 == nil and 'deleted' or val2 + hasChanged = true + end + end + + return diff, hasChanged +end + +local function sendPropsDiff() + if not Entity(vehicle).state.persisted then return end + local newProps = lib.getVehicleProperties(vehicle) + if not cachedProps then + cachedProps = newProps + return + end + local diff, hasChanged = calculateDiff(cachedProps, newProps) + cachedProps = newProps + if not hasChanged then return end + TriggerServerEvent('qbx_core:server:vehiclePropsChanged', netId, diff) +end + +lib.onCache('seat', function(newSeat) + if newSeat == -1 then + seat = -1 + vehicle = cache.vehicle + netId = NetworkGetNetworkIdFromEntity(vehicle) + CreateThread(function() + while seat == -1 do + sendPropsDiff() + Wait(10000) + end + end) + elseif seat == -1 then + seat = nil + sendPropsDiff() + vehicle = nil + netId = nil + end +end) \ No newline at end of file diff --git a/fxmanifest.lua b/fxmanifest.lua index 7cb8ea57a..9c5581591 100644 --- a/fxmanifest.lua +++ b/fxmanifest.lua @@ -23,6 +23,7 @@ client_scripts { 'client/events.lua', 'client/character.lua', 'client/discord.lua', + 'client/vehicle-persistence.lua', 'bridge/qb/client/main.lua', } @@ -36,6 +37,7 @@ server_scripts { 'server/commands.lua', 'server/loops.lua', 'server/character.lua', + 'server/vehicle-persistence.lua', 'bridge/qb/server/main.lua', } diff --git a/modules/lib.lua b/modules/lib.lua index 9351dbbb2..a61e9df94 100644 --- a/modules/lib.lua +++ b/modules/lib.lua @@ -316,7 +316,7 @@ if isServer then end local netId = NetworkGetNetworkIdFromEntity(veh) - + exports.qbx_core:EnablePersistence(veh) return netId, veh end else diff --git a/server/commands.lua b/server/commands.lua index 259676bfc..34fa656ee 100644 --- a/server/commands.lua +++ b/server/commands.lua @@ -146,7 +146,7 @@ lib.addCommand('car', { local keepCurrentVehicle = args[locale('command.car.params.keepCurrentVehicle.name')] local currentVehicle = not keepCurrentVehicle and GetVehiclePedIsIn(ped, false) if currentVehicle and currentVehicle ~= 0 then - DeleteEntity(currentVehicle) + DeleteVehicle(currentVehicle) end local _, vehicle = qbx.spawnVehicle({ @@ -180,7 +180,7 @@ lib.addCommand('dv', { for i = 1, #pedCars do local pedCar = NetworkGetEntityFromNetworkId(pedCars[i]) if pedCar and DoesEntityExist(pedCar) then - DeleteEntity(pedCar) + DeleteVehicle(pedCar) end end end diff --git a/server/functions.lua b/server/functions.lua index 297beb671..e5a57d845 100644 --- a/server/functions.lua +++ b/server/functions.lua @@ -483,4 +483,15 @@ local function getGroupMembers(group, type) return storage.fetchGroupMembers(group, type) end -exports('GetGroupMembers', getGroupMembers) \ No newline at end of file +exports('GetGroupMembers', getGroupMembers) + +---Disables persistence before deleting a vehicle, then deletes it. +---@param vehicle number +function DeleteVehicle(vehicle) + DisablePersistence(vehicle) + if DoesEntityExist(vehicle) then + DeleteEntity(vehicle) + end +end + +exports('DeleteVehicle', DeleteVehicle) \ No newline at end of file diff --git a/server/vehicle-persistence.lua b/server/vehicle-persistence.lua new file mode 100644 index 000000000..8369427b5 --- /dev/null +++ b/server/vehicle-persistence.lua @@ -0,0 +1,126 @@ +---A persisted vehicle will respawn when deleted. Only works for player owned vehicles. +---Vehicles spawned using lib are automatically persisted +---@param vehicle number +local function enablePersistence(vehicle) + Entity(vehicle).state:set('persisted', true, true) +end + +exports('EnablePersistence', enablePersistence) + +---A vehicle without persistence will not respawn when deleted. +---@param vehicle number +function DisablePersistence(vehicle) + Entity(vehicle).state:set('persisted', nil, true) +end + +exports('DisablePersistence', DisablePersistence) + +if GetConvar('qbx:enableVehiclePersistence', 'false') == 'false' then return end + +assert(lib.checkDependency('qbx_vehicles', '1.4.1', true)) + +local function getVehicleId(vehicle) + return Entity(vehicle).state.vehicleid or exports.qbx_vehicles:GetVehicleIdByPlate(GetVehicleNumberPlateText(vehicle)) +end + +RegisterNetEvent('qbx_core:server:vehiclePropsChanged', function(netId, diff) + local vehicle = NetworkGetEntityFromNetworkId(netId) + + local vehicleId = getVehicleId(vehicle) + if not vehicleId then return end + + local props = exports.qbx_vehicles:GetPlayerVehicle(vehicleId)?.props + if not props then return end + + if diff.bodyHealth then + props.bodyHealth = GetVehicleBodyHealth(vehicle) + end + + if diff.engineHealth then + props.engineHealth = GetVehicleEngineHealth(vehicle) + end + + if diff.tankHealth then + props.tankHealth = GetVehiclePetrolTankHealth(vehicle) + end + + if diff.fuelLevel then + props.fuelLevel = diff.fuelLevel ~= 'deleted' and diff.fuelLevel or nil + end + + if diff.oilLevel then + props.oilLevel = diff.oilLevel ~= 'deleted' and diff.oilLevel or nil + end + + if diff.dirtLevel then + props.dirtLevel = GetVehicleDirtLevel(vehicle) + end + + if diff.windows then + props.windows = diff.windows ~= 'deleted' and diff.windows or nil + end + + if diff.doors then + props.doors = diff.doors ~= 'deleted' and diff.doors or nil + end + + if diff.tyres then + local damage = {} + for i = 0, 7 do + if IsVehicleTyreBurst(vehicle, i, false) then + damage[i] = IsVehicleTyreBurst(vehicle, i, true) and 2 or 1 + end + end + + props.tyres = damage + end + + exports.qbx_vehicles:SaveVehicle(vehicle, { + props = props, + }) +end) + +local function getPedsInVehicleSeats(vehicle) + local occupants = {} + local occupantsI = 1 + for i = -1, 7 do + local ped = GetPedInVehicleSeat(vehicle, i) + if ped ~= 0 then + occupants[occupantsI] = { + ped = ped, + seat = i, + } + occupantsI += 1 + end + end + return occupants +end + +AddEventHandler('entityRemoved', function(entity) + if not Entity(entity).state.persisted then return end + local coords = GetEntityCoords(entity) + local heading = GetEntityHeading(entity) + local bucket = GetEntityRoutingBucket(entity) + local passengers = getPedsInVehicleSeats(entity) + + local vehicleId = getVehicleId(entity) + if not vehicleId then return end + local playerVehicle = exports.qbx_vehicles:GetPlayerVehicle(vehicleId) + + if DoesEntityExist(entity) then + Entity(entity).state:set('persisted', nil, true) + DeleteVehicle(entity) + end + + local _, veh = qbx.spawnVehicle({ + model = playerVehicle.props.model, + spawnSource = vec4(coords.x, coords.y, coords.z, heading), + bucket = bucket, + props = playerVehicle.props + }) + + for i = 1, #passengers do + local passenger = passengers[i] + SetPedIntoVehicle(passenger.ped, veh, passenger.seat) + end +end) \ No newline at end of file