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 12 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:enable_vehicle_persistence', 'true') == '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
3 changes: 2 additions & 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)

TriggerEvent('qbx_core:server:vehicleSpawned', veh)
return netId, veh
end
else
Expand Down Expand Up @@ -419,6 +419,7 @@ else
end

---Deletes the specified vehicle and returns whether it was successful.
---@deprecated use exports.qbx_core:DeleteVehicle(vehicle) instead
Manason marked this conversation as resolved.
Show resolved Hide resolved
---@param vehicle integer
---@return boolean deleted
function qbx.deleteVehicle(vehicle)
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
15 changes: 14 additions & 1 deletion server/functions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -483,4 +483,17 @@ 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)
if DisablePersistence then
DisablePersistence(vehicle)
end
if DoesEntityExist(vehicle) then
DeleteEntity(vehicle)
end
end

exports('DeleteVehicle', DeleteVehicle)
108 changes: 108 additions & 0 deletions server/vehicle-persistence.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
if GetConvar('qbx:enable_vehicle_persistence', 'false') == 'false' then return end

assert(lib.checkDependency('qbx_vehicles', '1.4.1', true))

---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)

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)

AddEventHandler('qbx_core:server:vehicleSpawned', function(entity)
Manason marked this conversation as resolved.
Show resolved Hide resolved
Entity(entity).state:set('persisted', true, true)
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 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

qbx.spawnVehicle({
model = playerVehicle.props.model,
spawnSource = vec4(coords.x, coords.y, coords.z, heading),
bucket = bucket,
props = playerVehicle.props
})
end)