Skip to content

feat: full persistence #677

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
40 changes: 39 additions & 1 deletion client/vehicle-persistence.lua
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
if GetConvar('qbx:enableVehiclePersistence', 'false') == 'false' then return end
local enable = GetConvar('qbx:enableVehiclePersistence', 'false') == 'true'
local full = GetConvar('qbx:vehiclePersistenceType', 'semi') == 'full'

if not enable then return end

local cachedProps
local netId
local vehicle
local seat

local zones = {}
local watchedKeys = {
'bodyHealth',
'engineHealth',
Expand Down Expand Up @@ -43,17 +47,37 @@ end

local function sendPropsDiff()
if not Entity(vehicle).state.persisted then return end

if full then TriggerServerEvent('qbx_core:server:vehiclePositionChanged', netId) 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

---@param vehicles table
local function createVehicleZones(vehicles)
for id, coords in pairs(vehicles) do
if not zones[id] then
zones[id] = lib.points.new({
distance = 75.0,
coords = coords,
onEnter = function()
TriggerServerEvent('qbx_core:server:spawnVehicle', id, coords)
end
})
end
end
end

lib.onCache('seat', function(newSeat)
if newSeat == -1 then
seat = -1
Expand All @@ -71,4 +95,18 @@ lib.onCache('seat', function(newSeat)
vehicle = nil
netId = nil
end
end)

AddEventHandler('QBCore:Client:OnPlayerLoaded', function()
local vehicles = lib.callback.await('qbx_core:server:getVehiclesToSpawn', 2500)
if not vehicles then return end

createVehicleZones(vehicles)
end)

RegisterNetEvent('qbx_core:client:removeVehZone', function(id)
if not zones[id] then return end

zones[id]:remove()
zones[id] = nil
end)
8 changes: 8 additions & 0 deletions config/server.lua
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,18 @@ return {
role = {} -- Role to tag for high priority logs. Roles use <@%roleid> and users/channels are <@userid/channelid>
},

persistence = {
lockState = 'lock', -- 'lock' : vehicle will be locked when spawned, 'unlock' : vehicle will be unlocked when spawned
},

giveVehicleKeys = function(src, plate, vehicle)
return exports.qbx_vehiclekeys:GiveKeys(src, vehicle)
end,

setVehicleLock = function(vehicle, state)
exports.qbx_vehiclekeys:SetLockState(vehicle, state)
end,

getSocietyAccount = function(accountName)
return exports['Renewed-Banking']:getAccountMoney(accountName)
end,
Expand Down
151 changes: 150 additions & 1 deletion server/vehicle-persistence.lua
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
local enable = GetConvar('qbx:enableVehiclePersistence', 'false') == 'true'
local full = GetConvar('qbx:vehiclePersistenceType', 'semi') == 'full'

---A persisted vehicle will respawn when deleted. Only works for player owned vehicles.
---Vehicles spawned using lib are automatically persisted
---@param vehicle number
Expand All @@ -15,7 +18,7 @@ end

exports('DisablePersistence', DisablePersistence)

if GetConvar('qbx:enableVehiclePersistence', 'false') == 'false' then return end
if not enable then return end

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

Expand Down Expand Up @@ -121,4 +124,150 @@ AddEventHandler('entityRemoved', function(entity)
local passenger = passengers[i]
SetPedIntoVehicle(passenger.ped, veh, passenger.seat)
end
end)

if not full then return end

local cachedVehicles = {}
local config = require 'config.server'

---@param plate string
---@return boolean
local function isVehicleSpawned(plate)
local vehicles = GetGamePool('CVehicle')

for i = 1, #vehicles do
local vehicle = vehicles[i]
if qbx.getVehiclePlate(vehicle) == plate then
return true
end
end

return false
end

--- Save the vehicle position to the database
---@param vehicle number
---@param coords vector3
---@param heading number
local function saveVehicle(vehicle, coords, heading)
local vehicleId = getVehicleId(vehicle)
if not vehicleId then return end

local props = exports.qbx_vehicles:GetPlayerVehicle(vehicleId)?.props
if not props then return end

local type = GetVehicleType(vehicle)

props.bodyHealth = GetVehicleBodyHealth(vehicle)
props.engineHealth = GetVehicleEngineHealth(vehicle)
props.tankHealth = GetVehiclePetrolTankHealth(vehicle)
props.dirtLevel = GetVehicleDirtLevel(vehicle)

if type == 'heli' or type == 'plane' then
coords = vec3(coords.x, coords.y, coords.z + 1.0)
end

exports.qbx_vehicles:SaveVehicle(vehicle, {
props = props,
coords = vec4(coords.x, coords.y, coords.z, heading)
})
end

--- Save all vehicle positions to the database
local function saveAllVehicle()
local vehicles = GetGamePool('CVehicle')
for i = 1, #vehicles do
local vehicle = vehicles[i]
if DoesEntityExist(vehicle) and Entity(vehicle).state.persisted then
saveVehicle(vehicle, GetEntityCoords(vehicle), GetEntityHeading(vehicle))
end
end
end

---@param coords vector4
---@param id number
---@param model string
---@param props table
local function spawnVehicle(coords, id, model, props)
if not coords or not id or not model or not props then return end

local _, veh = qbx.spawnVehicle({
spawnSource = vec4(coords.x, coords.y, coords.z, coords.w),
model = model,
props = props
})

cachedVehicles[id] = nil
Entity(veh).state:set('vehicleid', id, false)
TriggerClientEvent('qbx_core:client:removeVehZone', -1, id)
config.setVehicleLock(veh, config.persistence.lockState)
end

lib.callback.register('qbx_core:server:getVehiclesToSpawn', function()
return cachedVehicles
end)

AddEventHandler('onResourceStart', function(resourceName)
if resourceName ~= 'qbx_vehicles' then return end

local vehicles = exports.qbx_vehicles:GetPlayerVehicles({ states = 0 })
if not vehicles then return end

for i = 1, #vehicles do
local vehicle = vehicles[i]
if vehicle.coords and vehicle.props and vehicle.props.plate and not isVehicleSpawned(vehicle.props.plate) then
cachedVehicles[vehicle.id] = vehicle.coords
end
end
end)

AddEventHandler('onResourceStop', function(resourceName)
if resourceName ~= cache.resource then return end

saveAllVehicle()
end)

AddEventHandler('txAdmin:events:scheduledRestart', function(eventData)
if eventData.secondsRemaining ~= 60 then return end

saveAllVehicle()
end)

RegisterNetEvent('qbx_core:server:spawnVehicle', function(id, coords)
if not id or not coords then return end

local cachedCoords = cachedVehicles[id]
if not cachedCoords or
cachedCoords.x ~= coords.x or
cachedCoords.y ~= coords.y or
cachedCoords.z ~= coords.z or
cachedCoords.w ~= coords.w then
return
end

local vehicle = exports.qbx_vehicles:GetPlayerVehicle(id)
if not vehicle or not vehicle.modelName or not vehicle.props then return end

spawnVehicle(coords, id, vehicle.modelName, vehicle.props)
end)

RegisterNetEvent('qbx_core:server:vehiclePositionChanged', function(netId)
local src = source

local ped = GetPlayerPed(src)
local vehicle = NetworkGetEntityFromNetworkId(netId)

local vehicleId = getVehicleId(vehicle)
if not vehicleId then return end

local pedCoords = GetEntityCoords(ped)
local vehicleCoords = GetEntityCoords(vehicle)
local vehicleHeading = GetEntityHeading(vehicle)

if #(pedCoords - vehicleCoords) > 10.0 then
return
end

saveVehicle(vehicle, vehicleCoords, vehicleHeading)
end)