Skip to content

Commit

Permalink
feat: persistent player vehicles (#568)
Browse files Browse the repository at this point in the history
* feat: persistent player vehicles

* fix: linting

* removed from manifest

* fix: driver switching seats or exiting breaks loop only

* feat: persistence tracking

* use statebag instead of server table

* fix(server/vehicle-persistence): typo

* fix(server/vehicle-persistence): remove state only when entity exists

* disabling by default

* delete vehicles using internal function

* deprecate qbx.deleteVehicle

* revert: deprecating qbx.deleteVehicle

* feat: warp passengers back into a vehicle on removal

* use persistence exports instead of event and make them always available

* camelCasing convar and defaulting to false

---------

Co-authored-by: David Malchin <malchin459@gmail.com>
  • Loading branch information
Manason and D4isDAVID authored Sep 19, 2024
1 parent cf975d0 commit db3b000
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 4 deletions.
74 changes: 74 additions & 0 deletions client/vehicle-persistence.lua
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 2 additions & 0 deletions fxmanifest.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ client_scripts {
'client/events.lua',
'client/character.lua',
'client/discord.lua',
'client/vehicle-persistence.lua',
'bridge/qb/client/main.lua',
}

Expand All @@ -36,6 +37,7 @@ server_scripts {
'server/commands.lua',
'server/loops.lua',
'server/character.lua',
'server/vehicle-persistence.lua',
'bridge/qb/server/main.lua',
}

Expand Down
2 changes: 1 addition & 1 deletion modules/lib.lua
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ if isServer then
end

local netId = NetworkGetNetworkIdFromEntity(veh)

exports.qbx_core:EnablePersistence(veh)
return netId, veh
end
else
Expand Down
4 changes: 2 additions & 2 deletions server/commands.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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
Expand Down
13 changes: 12 additions & 1 deletion server/functions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -483,4 +483,15 @@ local function getGroupMembers(group, type)
return storage.fetchGroupMembers(group, type)
end

exports('GetGroupMembers', getGroupMembers)
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)
126 changes: 126 additions & 0 deletions server/vehicle-persistence.lua
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit db3b000

Please sign in to comment.