Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: persistent player vehicles #568

Merged
merged 16 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
solareon marked this conversation as resolved.
Show resolved Hide resolved
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()
solareon marked this conversation as resolved.
Show resolved Hide resolved
vehicle = nil
netId = nil
end
end)
Manason marked this conversation as resolved.
Show resolved Hide resolved
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)