905 lines
35 KiB
Lua
905 lines
35 KiB
Lua
-- ============================================================
|
||
-- sync.lua – Persistent vehicle management (server-side)
|
||
-- ============================================================
|
||
|
||
PersistentVehicles = {}
|
||
SpawnTimeout = nil
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- Helpers
|
||
-- ──────────────────────────────────────────────
|
||
|
||
local function NormalizeVehicleType(vehicleType)
|
||
local map = {
|
||
car = "vehicle",
|
||
automobile = "vehicle",
|
||
pdm = "vehicle",
|
||
airplane = "plane",
|
||
air = "plane",
|
||
helicopter = "plane",
|
||
heli = "plane",
|
||
water = "boat",
|
||
}
|
||
return map[vehicleType] or vehicleType
|
||
end
|
||
|
||
local function GetVehicleCoordsFromProps(vehicleRow)
|
||
local propsColumn = garagePropsColumn
|
||
local rawProps = vehicleRow[propsColumn]
|
||
local props = rawProps and json.decode(rawProps)
|
||
if props then
|
||
return props.coords
|
||
end
|
||
return nil
|
||
end
|
||
|
||
local function ImpoundAndDeleteIfSpawned(plate, impoundGarage, statusValue)
|
||
local query = string.format(
|
||
"UPDATE %s SET `garage` = ?, `%s` = ? WHERE plate = ?",
|
||
garageTable, storedColumn
|
||
)
|
||
MySQL.Sync.execute(query, { impoundGarage, statusValue, plate })
|
||
|
||
local vehicleKey = GetVehicleFromPlate(plate)
|
||
if vehicleKey and PersistentVehicles[vehicleKey] then
|
||
local entity = PersistentVehicles[vehicleKey].entity
|
||
if DoesEntityExist(entity) then
|
||
DeleteEntity(entity)
|
||
end
|
||
PersistentVehicles[vehicleKey] = nil
|
||
end
|
||
end
|
||
|
||
-- Deep-clone a table recursively
|
||
local function DeepClone(tbl)
|
||
local clone = table.clone(tbl)
|
||
for key, value in pairs(clone) do
|
||
if type(value) == "table" then
|
||
clone[key] = DeepClone(value)
|
||
end
|
||
end
|
||
return clone
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- GetVehicleFromPlate (key → index in PersistentVehicles)
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function GetVehicleFromPlate(plate)
|
||
for key, vehicleData in pairs(PersistentVehicles) do
|
||
if vehicleData.vehicle and vehicleData.vehicle.plate == plate then
|
||
return key
|
||
end
|
||
end
|
||
return nil
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- GetPlayersFromRoutingZero
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function GetPlayersFromRoutingZero()
|
||
local routingZeroPlayers = {}
|
||
for _, playerSource in pairs(Players) do
|
||
if GetPlayerRoutingBucket(playerSource) == 0 then
|
||
routingZeroPlayers[#routingZeroPlayers + 1] = playerSource
|
||
end
|
||
end
|
||
local allInRoutingZero = #routingZeroPlayers == #Players
|
||
return routingZeroPlayers, allInRoutingZero
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- SpawnVehicle
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function SpawnVehicle(dbId, ownerIdentifier, vehicleType, spawnCoords, vehicleProps, targetPlayer, setKeys)
|
||
vehicleType = NormalizeVehicleType(vehicleType)
|
||
|
||
if not CreateVehicleServerSetter then
|
||
print("[GARAGES] ERREUR CRITIQUE: La fonction CreateVehicleServerSetter n'existe pas sur votre serveur !")
|
||
return false
|
||
end
|
||
|
||
local vehicle = CreateVehicleServerSetter(vehicleProps.model, vehicleType, spawnCoords.x, spawnCoords.y, spawnCoords.z, spawnCoords.w or 0.0)
|
||
|
||
if not DoesEntityExist(vehicle) then
|
||
Error("SpawnVehicle ::: Failed to create vehicle entity. Model:", vehicleProps.model)
|
||
return false
|
||
end
|
||
|
||
local netId = NetworkGetNetworkIdFromEntity(vehicle)
|
||
local plateStr = MathTrim(vehicleProps.plate)
|
||
|
||
SetVehicleNumberPlateText(vehicle, plateStr)
|
||
|
||
local persistentEntry = {
|
||
id = dbId,
|
||
type = vehicleType,
|
||
entity = vehicle,
|
||
netID = netId,
|
||
vehicle = vehicleProps,
|
||
plate = plateStr,
|
||
owner = ownerIdentifier,
|
||
}
|
||
PersistentVehicles[plateStr] = persistentEntry
|
||
|
||
if setKeys and targetPlayer then
|
||
TriggerClientEvent("advancedgarages:giveKeys", targetPlayer, netId, plateStr)
|
||
end
|
||
|
||
Debug("SpawnVehicle ::: Spawned vehicle. Plate:", plateStr, "NetID:", netId)
|
||
return persistentEntry
|
||
end
|
||
exports("SpawnVehicle", SpawnVehicle)
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- setVehicleToPersistent (export)
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function setVehicleToPersistent(netId)
|
||
local entity = NetworkGetEntityFromNetworkId(netId)
|
||
local plate = MathTrim(GetVehicleNumberPlateText(entity))
|
||
|
||
local query = "SELECT * FROM " .. garageTable .. " WHERE plate = ?"
|
||
local result = MySQL.Sync.fetchAll(query, { plate })
|
||
|
||
if not result[1] then
|
||
print("setVehicleToPersistent ::: The vehicle is not in the database")
|
||
return false
|
||
end
|
||
|
||
local row = result[1]
|
||
local vehicleCol = (Config.Framework == "qb") and "mods" or "vehicle"
|
||
local props = json.decode(row[vehicleCol])
|
||
|
||
if not props then
|
||
print("setVehicleToPersistent ::: Does not vehicle data exist vehicle col is", vehicleCol)
|
||
return false
|
||
end
|
||
|
||
PersistentVehicles[plate] = {
|
||
id = row.id,
|
||
type = row.type,
|
||
entity = entity,
|
||
netID = netId,
|
||
vehicle = props,
|
||
plate = plate,
|
||
owner = row.owner,
|
||
}
|
||
return true
|
||
end
|
||
exports("setVehicleToPersistent", setVehicleToPersistent)
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- removeVehicleFromPersistent (export)
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function removeVehicleFromPersistent(plate, targetGarage)
|
||
local vehicleKey = GetVehicleFromPlate(plate)
|
||
|
||
if not vehicleKey then
|
||
Error("removeVehicleFromPersistent ::: Vehicle is not persistent or spawned, plate:", plate)
|
||
return false
|
||
end
|
||
|
||
local statusValue = State.stored
|
||
|
||
if not targetGarage then
|
||
Debug("No garage was specified trying to send the vehicle to the impound")
|
||
local vehicleData = PersistentVehicles[vehicleKey]
|
||
targetGarage = GetImpoundOfType(vehicleData.type, vehicleData.vehicle.coords)
|
||
if not targetGarage then
|
||
Error("No impound for type:", vehicleData.type, "coords", vehicleData.vehicle.coords)
|
||
return false
|
||
end
|
||
statusValue = State.impounded
|
||
end
|
||
|
||
local vehicleProps = PersistentVehicles[vehicleKey].vehicle
|
||
local query = string.format(
|
||
"UPDATE %s SET `garage` = ?, `%s` = ?, `%s` = ? WHERE plate = ?",
|
||
garageTable, storedColumn, garagePropsColumn
|
||
)
|
||
MySQL.Sync.execute(query, { targetGarage, statusValue, json.encode(vehicleProps), plate })
|
||
|
||
Debug("Vehicle saved in garage:", targetGarage, "plate:", plate)
|
||
|
||
local entity = PersistentVehicles[vehicleKey].entity
|
||
if DoesEntityExist(entity) then
|
||
DeleteEntity(entity)
|
||
end
|
||
PersistentVehicles[vehicleKey] = nil
|
||
return true
|
||
end
|
||
exports("removeVehicleFromPersistent", removeVehicleFromPersistent)
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- Debug command: /addtopers
|
||
-- ──────────────────────────────────────────────
|
||
|
||
RegisterCommand("addtopers", function(sourcePlayer)
|
||
local ped = GetPlayerPed(sourcePlayer)
|
||
local vehicle = GetVehiclePedIsIn(ped, false)
|
||
if not DoesEntityExist(vehicle) then return end
|
||
|
||
local netId = NetworkGetNetworkIdFromEntity(vehicle)
|
||
local result = exports["qs-advancedgarages"]:setVehicleToPersistent(netId)
|
||
if result then
|
||
print("yay")
|
||
end
|
||
end, false)
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- Debug command: /tptoplate
|
||
-- ──────────────────────────────────────────────
|
||
|
||
RegisterCommand("tptoplate", function(sourcePlayer, args)
|
||
if not PlayerIsAdmin(sourcePlayer) then
|
||
return print("You are not admin")
|
||
end
|
||
if not args then
|
||
return print("No plate")
|
||
end
|
||
|
||
local plate = table.concat(args, " ")
|
||
local vehicleKey = GetVehicleFromPlate(plate)
|
||
if not vehicleKey then
|
||
return print("Vehicle not found")
|
||
end
|
||
|
||
local coords = PersistentVehicles[vehicleKey].vehicle.coords
|
||
SetEntityCoords(GetPlayerPed(sourcePlayer), coords.x, coords.y, coords.z)
|
||
Debug("Teleported to:", plate)
|
||
end, false)
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- Startup: send "OUT" garage vehicles to impound
|
||
-- (PersistentVehicles mode)
|
||
-- ──────────────────────────────────────────────
|
||
|
||
if Config.PersistentVehicles then
|
||
CreateThread(function()
|
||
Wait(1000)
|
||
|
||
local selectQuery = string.format("SELECT * FROM %s WHERE garage = 'OUT'", garageTable)
|
||
local updateQuery = string.format(
|
||
"UPDATE %s SET `garage` = ?, `%s` = ? WHERE plate = ?",
|
||
garageTable, storedColumn
|
||
)
|
||
|
||
local spawnedPlates = {}
|
||
|
||
-- Wait until at least one player in routing bucket 0
|
||
while true do
|
||
if initializedStartup then break end
|
||
|
||
local players, allInZero = GetPlayersFromRoutingZero()
|
||
if not allInZero then
|
||
Debug("Not any player has routing 0, waiting...")
|
||
end
|
||
|
||
if #players > 0 then
|
||
initializedStartup = true
|
||
|
||
local outVehicles = MySQL.Sync.fetchAll(selectQuery)
|
||
for _, row in pairs(outVehicles) do
|
||
local ownerIdent = row[garageIdentifierColumn]
|
||
if Config.Framework == "qb" then
|
||
row.vehicle = row.mods
|
||
end
|
||
|
||
local props = json.decode(row.vehicle)
|
||
if props then
|
||
row.type = NormalizeVehicleType(row.type)
|
||
local propsCoords = json.decode(row[garagePropsColumn] or "")
|
||
local vehicleCoords = (propsCoords and propsCoords.coords) or nil
|
||
local impound = GetImpoundOfType(row.type, vehicleCoords)
|
||
|
||
if not impound then
|
||
Debug("No impound for type:", row.type)
|
||
elseif not props.coords then
|
||
MySQL.Sync.execute(updateQuery, { impound, State.impounded, row.plate })
|
||
Debug("Vehicle without coordinates:", row.plate, ", sent to:", impound)
|
||
else
|
||
local spawnedEntry = SpawnVehicle(row.id, ownerIdent, row.type, props.coords, props)
|
||
table.insert(spawnedPlates, spawnedEntry)
|
||
end
|
||
end
|
||
end
|
||
break
|
||
end
|
||
|
||
Wait(5000)
|
||
end
|
||
|
||
TriggerClientEvent("advancedgarages:updatePersistentVehicleBlips", -1, PersistentVehicles)
|
||
end)
|
||
else
|
||
-- Non-persistent mode: send all OUT vehicles to impound at startup
|
||
CreateThread(function()
|
||
local count = 0
|
||
local selectQuery = string.format("SELECT * FROM %s WHERE garage = 'OUT'", garageTable)
|
||
local updateQuery = string.format(
|
||
"UPDATE %s SET `garage` = ?, `%s` = ? WHERE garage = 'OUT'",
|
||
garageTable, storedColumn
|
||
)
|
||
|
||
local outVehicles = MySQL.Sync.fetchAll(selectQuery)
|
||
for _, row in pairs(outVehicles) do
|
||
local vehicleCoords = GetVehicleCoordsFromProps(row)
|
||
local impound = GetImpoundOfType(row.type, vehicleCoords)
|
||
local isSpawned = GetVehicleFromPlate(row.plate)
|
||
|
||
if not impound then
|
||
Debug("No impound for type:", row.type)
|
||
elseif isSpawned then
|
||
Debug("Vehicle:", row.plate, " was found in the server, we don't send it to the impound.")
|
||
else
|
||
count = count + 1
|
||
MySQL.Sync.execute(updateQuery, { impound, State.impounded })
|
||
end
|
||
end
|
||
|
||
if count > 0 then
|
||
Debug("All vehicles in the OUT garage have been sent to their respective impound. Total: " .. count)
|
||
end
|
||
end)
|
||
end
|
||
|
||
-- Startup: send "OUT" plane-type vehicles to impound
|
||
CreateThread(function()
|
||
local count = 0
|
||
local selectQuery = string.format("SELECT * FROM %s WHERE garage = 'OUT' AND type = 'plane'", garageTable)
|
||
local updateQuery = string.format(
|
||
"UPDATE %s SET `garage` = ?, `%s` = ? WHERE garage = 'OUT' AND type = 'plane'",
|
||
garageTable, storedColumn
|
||
)
|
||
|
||
local outPlanes = MySQL.Sync.fetchAll(selectQuery)
|
||
for _, row in pairs(outPlanes) do
|
||
local vehicleCoords = GetVehicleCoordsFromProps(row)
|
||
local impound = GetImpoundOfType(row.type, vehicleCoords)
|
||
local isSpawned = GetVehicleFromPlate(row.plate)
|
||
|
||
if not impound then
|
||
Debug("No impound for type:", row.type)
|
||
elseif isSpawned then
|
||
-- vehicle already on server, skip
|
||
else
|
||
count = count + 1
|
||
MySQL.Sync.execute(updateQuery, { impound, State.impounded })
|
||
end
|
||
end
|
||
end)
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- GetNearbyPlayerFromCoords
|
||
-- ──────────────────────────────────────────────
|
||
|
||
local function GetNearbyPlayerFromCoords(coords)
|
||
local position = vec3(coords.x, coords.y, coords.z)
|
||
local maxRadius = 70.0
|
||
|
||
for _, playerSource in pairs(Players) do
|
||
local ped = GetPlayerPed(playerSource)
|
||
if GetEntityRoutingBucket(ped) == 0 then
|
||
local pedCoords = vec3(table.unpack({ GetEntityCoords(ped) }))
|
||
if #(position - pedCoords) < maxRadius then
|
||
return playerSource
|
||
end
|
||
end
|
||
end
|
||
return nil
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- vehiclePropertiesSetted event
|
||
-- ──────────────────────────────────────────────
|
||
|
||
RegisterNetEvent("advancedgarages:vehiclePropertiesSetted")
|
||
AddEventHandler("advancedgarages:vehiclePropertiesSetted", function(plate)
|
||
local vehicleKey = GetVehicleFromPlate(plate)
|
||
if not vehicleKey then
|
||
Debug("Vehicle does not exist, plate:", plate, "persistentVehicles", PersistentVehicles)
|
||
return
|
||
end
|
||
PersistentVehicles[vehicleKey].propertiesSetted = true
|
||
end)
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- Persistent vehicle tick (properties + respawn)
|
||
-- ──────────────────────────────────────────────
|
||
|
||
local function TickPersistentVehicles()
|
||
local propertyQueue = {}
|
||
local respawnList = {}
|
||
local respawnedPlates = {}
|
||
|
||
for key, vehicleData in pairs(PersistentVehicles) do
|
||
if not DoesEntityExist(vehicleData.entity) then
|
||
Debug("The vehicle does not exist, trying to spawn it again...", vehicleData.plate)
|
||
table.insert(respawnList, {
|
||
index = key,
|
||
id = vehicleData.id,
|
||
owner = vehicleData.owner,
|
||
vType = vehicleData.type,
|
||
coords = vehicleData.vehicle.coords,
|
||
props = vehicleData.vehicle,
|
||
plate = vehicleData.plate,
|
||
deleteEntity = false,
|
||
entity = vehicleData.entity,
|
||
})
|
||
else
|
||
-- Update live state
|
||
local entityCoords = GetEntityCoords(vehicleData.entity)
|
||
local entityHeading = GetEntityHeading(vehicleData.entity)
|
||
PersistentVehicles[key].vehicle.coords = vec4(entityCoords.x, entityCoords.y, entityCoords.z, entityHeading)
|
||
|
||
local entityState = Entity(vehicleData.entity).state
|
||
PersistentVehicles[key].vehicle.deformations = entityState.deformations or {}
|
||
|
||
if Config.Fuel == "none" then
|
||
PersistentVehicles[key].vehicle.fuel = entityState.fuel or 100.0
|
||
end
|
||
|
||
PersistentVehicles[key].vehicle.engineHealth = GetVehicleEngineHealth(vehicleData.entity)
|
||
PersistentVehicles[key].vehicle.bodyHealth = GetVehicleBodyHealth(vehicleData.entity)
|
||
PersistentVehicles[key].vehicle.locked = GetVehicleDoorLockStatus(vehicleData.entity)
|
||
|
||
-- Handle property retries
|
||
if not vehicleData.propertiesSetted then
|
||
PersistentVehicles[key].propertySetRetries = (PersistentVehicles[key].propertySetRetries or 0) + 1
|
||
local retries = PersistentVehicles[key].propertySetRetries
|
||
|
||
if retries > 15 then
|
||
Debug("Property set retries exceeded for plate:", vehicleData.plate, "- force respawning")
|
||
table.insert(respawnList, {
|
||
index = key,
|
||
id = vehicleData.id,
|
||
owner = vehicleData.owner,
|
||
vType = vehicleData.type,
|
||
coords = vehicleData.vehicle.coords,
|
||
props = vehicleData.vehicle,
|
||
plate = vehicleData.plate,
|
||
deleteEntity = true,
|
||
entity = vehicleData.entity,
|
||
})
|
||
else
|
||
local trimmedPlate = MathTrim(vehicleData.plate)
|
||
if not trimmedPlate then
|
||
Debug("There was a problem loading the plate for said vehicle...")
|
||
return
|
||
end
|
||
SetVehicleNumberPlateText(vehicleData.entity, trimmedPlate)
|
||
|
||
local nearbyPlayer = GetNearbyPlayerFromCoords(vehicleData.vehicle.coords)
|
||
if nearbyPlayer then
|
||
local clonedData = DeepClone(vehicleData)
|
||
clonedData.source = nearbyPlayer
|
||
|
||
if not propertyQueue[nearbyPlayer] then
|
||
propertyQueue[nearbyPlayer] = {}
|
||
end
|
||
table.insert(propertyQueue[nearbyPlayer], clonedData)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Respawn vehicles
|
||
if #respawnList > 0 then
|
||
for _, entry in ipairs(respawnList) do
|
||
if entry.deleteEntity and entry.entity then
|
||
if DoesEntityExist(entry.entity) then
|
||
DeleteEntity(entry.entity)
|
||
end
|
||
end
|
||
PersistentVehicles[entry.index] = nil
|
||
SpawnVehicle(entry.id, entry.owner, entry.vType, entry.coords, entry.props)
|
||
table.insert(respawnedPlates, entry.plate)
|
||
end
|
||
|
||
if #respawnedPlates > 0 then
|
||
Debug("The following vehicles have been respawned:", table.concat(respawnedPlates, ", "))
|
||
TriggerClientEvent("advancedgarages:updatePersistentVehicleBlips", -1, PersistentVehicles)
|
||
end
|
||
end
|
||
|
||
-- Send property requests to nearby players
|
||
if type(propertyQueue) == "table" and next(propertyQueue) ~= nil then
|
||
for playerSource, vehicleList in pairs(propertyQueue) do
|
||
TriggerClientEvent("advancedgarages:setVehicleProperties", playerSource, vehicleList)
|
||
end
|
||
end
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- updatePersistentVehicleProps (export)
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function updatePersistentVehicleProps(plate, newProps)
|
||
assert(type(plate) == "string", "updatePersistentVehicleProps ::: Plate must be a string")
|
||
|
||
local vehicleKey = GetVehicleFromPlate(plate)
|
||
assert(vehicleKey, "updatePersistentVehicleProps ::: Vehicle not found for plate: " .. plate)
|
||
|
||
local entity = PersistentVehicles[vehicleKey].entity
|
||
assert(DoesEntityExist(entity), "updatePersistentVehicleProps ::: Entity does not exist for vehicle: " .. plate)
|
||
|
||
PersistentVehicles[vehicleKey].vehicle = newProps
|
||
SaveAllPersistentVehicles()
|
||
Debug("updatePersistentVehicleProps ::: Updating vehicle properties for plate:", plate)
|
||
return true
|
||
end
|
||
exports("updatePersistentVehicleProps", updatePersistentVehicleProps)
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- getNearbyVehicle – find a vehicle near player
|
||
-- checks PersistentVehicles first, then all vehicles
|
||
-- ──────────────────────────────────────────────
|
||
|
||
local function FindNearbyVehicleFromAllVehicles(sourcePlayer)
|
||
local pedCoords = GetEntityCoords(GetPlayerPed(sourcePlayer))
|
||
|
||
for _, vehicleEntity in pairs(GetAllVehicles()) do
|
||
local coords = GetEntityCoords(vehicleEntity)
|
||
if not coords then
|
||
print("No GetEntityCoords")
|
||
return
|
||
end
|
||
|
||
if #(pedCoords - vec3(coords.x, coords.y, coords.z)) < 4 then
|
||
local plate = MathTrim(GetVehicleNumberPlateText(vehicleEntity))
|
||
local dbResult = MySQL.Sync.fetchAll(
|
||
"SELECT * FROM " .. garageTable .. " WHERE plate = @plate",
|
||
{ ["@plate"] = plate }
|
||
)
|
||
|
||
if dbResult[1] then
|
||
local vehicleType = NormalizeVehicleType(dbResult[1].type)
|
||
return vehicleEntity, vehicleType
|
||
end
|
||
end
|
||
end
|
||
return false
|
||
end
|
||
|
||
local function FindNearbyPersistentVehicle(sourcePlayer)
|
||
local pedCoords = GetEntityCoords(GetPlayerPed(sourcePlayer))
|
||
|
||
for key, vehicleData in pairs(PersistentVehicles) do
|
||
local vehicleCoords = vehicleData.vehicle.coords
|
||
if not vehicleCoords then
|
||
-- Try to get coords from entity
|
||
if DoesEntityExist(vehicleData.entity) then
|
||
local ec = GetEntityCoords(vehicleData.entity)
|
||
local eh = GetEntityHeading(vehicleData.entity)
|
||
vehicleData.vehicle.coords = vec4(ec.x, ec.y, ec.z, eh)
|
||
vehicleCoords = vehicleData.vehicle.coords
|
||
else
|
||
print("This vehicle is not exist but tried to impound wtf", vehicleData.plate)
|
||
end
|
||
end
|
||
|
||
if vehicleCoords then
|
||
local dist = #(pedCoords - vec3(vehicleCoords.x, vehicleCoords.y, vehicleCoords.z))
|
||
vehicleData.type = NormalizeVehicleType(vehicleData.type)
|
||
if dist < 4 then
|
||
return key, vehicleData.type
|
||
end
|
||
end
|
||
end
|
||
|
||
return FindNearbyVehicleFromAllVehicles(sourcePlayer)
|
||
end
|
||
|
||
lib.callback.register("advancedgarages:getNearbyVehicle", function(sourcePlayer)
|
||
return FindNearbyPersistentVehicle(sourcePlayer)
|
||
end)
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- Delete nearby vehicle (admin command + export)
|
||
-- ──────────────────────────────────────────────
|
||
|
||
local function DeleteNearbyVehicle(sourcePlayer)
|
||
local jobName = GetJobName(sourcePlayer)
|
||
local isAdmin = PlayerIsAdmin(sourcePlayer)
|
||
local isAllowed = IsJobAllowed(jobName, "impound")
|
||
|
||
if not isAllowed and not isAdmin then return end
|
||
|
||
local vehicleKey, vehicleType = FindNearbyPersistentVehicle(sourcePlayer)
|
||
|
||
-- No persistent vehicle nearby → try deleting any nearby entity
|
||
if not vehicleKey then
|
||
local pedCoords = GetEntityCoords(GetPlayerPed(sourcePlayer))
|
||
local nearest = nil
|
||
local minDist = 30.0
|
||
|
||
for _, vehicleEntity in pairs(GetAllVehicles()) do
|
||
local coords = GetEntityCoords(vehicleEntity)
|
||
local dist = #(pedCoords - vec3(coords.x, coords.y, coords.z))
|
||
if dist < minDist then
|
||
minDist = dist
|
||
nearest = vehicleEntity
|
||
end
|
||
end
|
||
|
||
if nearest then
|
||
if DoesEntityExist(nearest) then DeleteEntity(nearest) end
|
||
Notification(sourcePlayer, i18n.t("vehicle_removed"), "success")
|
||
else
|
||
Notification(sourcePlayer, i18n.t("no_vehicle_nearby"), "info")
|
||
end
|
||
return
|
||
end
|
||
|
||
-- Found persistent vehicle → impound it
|
||
local persistentData = PersistentVehicles[vehicleKey]
|
||
local plate = persistentData and persistentData.vehicle and persistentData.vehicle.plate
|
||
|
||
if not plate then
|
||
plate = MathTrim(GetVehicleNumberPlateText(vehicleKey))
|
||
end
|
||
|
||
local entityToDelete = (persistentData and persistentData.entity) or vehicleKey
|
||
local vehicleCoords = GetEntityCoords(entityToDelete)
|
||
local impound = GetImpoundOfType(vehicleType, vehicleCoords)
|
||
|
||
if not impound then
|
||
Debug("No impound for type:", vehicleType)
|
||
return
|
||
end
|
||
|
||
-- Check if it is a job vehicle with its own garage
|
||
local jobCheck = MySQL.Sync.fetchAll(
|
||
string.format("SELECT * FROM %s WHERE plate = ? AND jobGarage != ''", garageTable),
|
||
{ plate }
|
||
)
|
||
|
||
local updateQuery = string.format(
|
||
"UPDATE %s SET `garage` = ?, `%s` = ? WHERE plate = ?",
|
||
garageTable, storedColumn
|
||
)
|
||
|
||
if DoesEntityExist(entityToDelete) then DeleteEntity(entityToDelete) end
|
||
|
||
if jobCheck[1] then
|
||
local jobGarage = jobCheck[1].jobGarage
|
||
MySQL.Sync.execute(updateQuery, { jobGarage, State.stored, plate })
|
||
Notification(sourcePlayer, i18n.t("vehicle_impounded", { garage = jobGarage }), "success")
|
||
else
|
||
MySQL.Sync.execute(updateQuery, { impound, State.impounded, plate })
|
||
|
||
-- QB-Core state update
|
||
if Config.Framework == "qb" then
|
||
Debug("Vehicle goes to state 2 (IMPOUND)")
|
||
MySQL.update(
|
||
"UPDATE player_vehicles SET state = ?, depotprice = ? WHERE plate = ?",
|
||
{ 2, Config.ImpoundPrice, plate }
|
||
)
|
||
end
|
||
|
||
if persistentData then
|
||
PersistentVehicles[vehicleKey] = nil
|
||
end
|
||
|
||
Notification(sourcePlayer, i18n.t("vehicle_impounded", { garage = impound }), "success")
|
||
end
|
||
end
|
||
|
||
exports("deleteVehicle", DeleteNearbyVehicle)
|
||
RegisterCommand("mdv", DeleteNearbyVehicle)
|
||
|
||
RegisterCommand("mdvall", function(sourcePlayer)
|
||
if not PlayerIsAdmin(sourcePlayer) then return end
|
||
|
||
local updateQuery = string.format(
|
||
"UPDATE %s SET `garage` = ?, `%s` = ? WHERE plate = ?",
|
||
garageTable, storedColumn
|
||
)
|
||
|
||
for key, vehicleData in pairs(PersistentVehicles) do
|
||
local driverPed = GetPedInVehicleSeat(vehicleData.entity, -1)
|
||
if driverPed == 0 then
|
||
local impound = GetImpoundOfType(vehicleData.type, vehicleData.vehicle.coords)
|
||
MySQL.Sync.execute(updateQuery, { impound, State.impounded, vehicleData.vehicle.plate })
|
||
|
||
if DoesEntityExist(vehicleData.entity) then
|
||
DeleteEntity(vehicleData.entity)
|
||
end
|
||
PersistentVehicles[key] = nil
|
||
|
||
if Config.Framework == "qb" then
|
||
Debug("Vehicle goes to state 2 (IMPOUND)")
|
||
MySQL.update(
|
||
"UPDATE player_vehicles SET state = ?, depotprice = ? WHERE plate = ?",
|
||
{ 2, Config.ImpoundPrice, vehicleData.vehicle.plate }
|
||
)
|
||
end
|
||
end
|
||
end
|
||
|
||
Notification(sourcePlayer, i18n.t("all_vehicles_impounded"), "success")
|
||
end, false)
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- impound (net event)
|
||
-- ──────────────────────────────────────────────
|
||
|
||
RegisterNetEvent("advancedgarages:impound")
|
||
AddEventHandler("advancedgarages:impound", function(vehicleKey, impoundData)
|
||
local playerSource = source
|
||
local jobName = GetJobName(playerSource)
|
||
|
||
if not IsJobAllowed(jobName, "impound") then
|
||
Debug("Player is not allowed to impound", jobName)
|
||
return
|
||
end
|
||
|
||
impoundData.time = os.time() + impoundData.time
|
||
|
||
local updateQuery = string.format(
|
||
"UPDATE %s SET `garage` = ?, `%s` = ?, impound_data = ? WHERE plate = ?",
|
||
garageTable, storedColumn
|
||
)
|
||
|
||
local entityToImpound = vehicleKey
|
||
if PersistentVehicles[vehicleKey] then
|
||
entityToImpound = PersistentVehicles[vehicleKey].entity
|
||
end
|
||
|
||
if not DoesEntityExist(entityToImpound) then
|
||
Debug("Vehicle not spawned impound")
|
||
return
|
||
end
|
||
|
||
Debug("Impounding vehicle", impoundData.selectedGarage)
|
||
|
||
local plate = MathTrim(GetVehicleNumberPlateText(entityToImpound))
|
||
|
||
MySQL.Sync.execute(updateQuery, {
|
||
impoundData.selectedGarage,
|
||
State.impounded,
|
||
json.encode(impoundData),
|
||
plate,
|
||
})
|
||
|
||
if DoesEntityExist(entityToImpound) then
|
||
DeleteEntity(entityToImpound)
|
||
end
|
||
|
||
if PersistentVehicles[vehicleKey] then
|
||
PersistentVehicles[vehicleKey] = nil
|
||
end
|
||
|
||
SendWebhook(
|
||
Webhooks.impound_vehicle,
|
||
i18n.t("log.impound_vehicle.title"),
|
||
7393279,
|
||
i18n.t("log.impound_vehicle.description", {
|
||
player = GetPlayerName(playerSource),
|
||
job = jobName,
|
||
plate = plate,
|
||
garage = impoundData.selectedGarage,
|
||
})
|
||
)
|
||
|
||
if Config.Framework == "qb" then
|
||
Debug("Vehicle goes to state 2 (IMPOUND)")
|
||
MySQL.update(
|
||
"UPDATE player_vehicles SET state = ?, depotprice = ? WHERE plate = ?",
|
||
{ 2, impoundData.price, plate }
|
||
)
|
||
end
|
||
|
||
Notification(playerSource, i18n.t("vehicle_impounded", { garage = impoundData.selectedGarage }), "success")
|
||
end)
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- impound export (by plate)
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function ImpoundVehicleByPlate(plate)
|
||
plate = MathTrim(plate)
|
||
|
||
local vehicleKey = GetVehicleFromPlate(plate)
|
||
if not vehicleKey then
|
||
return Error("Vehicle not spawned, plate:", plate)
|
||
end
|
||
|
||
local vehicleData = PersistentVehicles[vehicleKey]
|
||
local impound = GetImpoundOfType(vehicleData.type, vehicleData.vehicle.coords)
|
||
|
||
local updateQuery = string.format(
|
||
"UPDATE %s SET `garage` = ?, `%s` = ? WHERE plate = ?",
|
||
garageTable, storedColumn
|
||
)
|
||
MySQL.Sync.execute(updateQuery, { impound, State.impounded, vehicleData.vehicle.plate })
|
||
|
||
local entity = vehicleData.entity
|
||
if DoesEntityExist(entity) then DeleteEntity(entity) end
|
||
PersistentVehicles[vehicleKey] = nil
|
||
end
|
||
exports("impound", ImpoundVehicleByPlate)
|
||
|
||
-- Server event alias
|
||
RegisterServerEvent("advancedgarages:server:impoundVehicle")
|
||
AddEventHandler("advancedgarages:server:impoundVehicle", function(plate, impoundData)
|
||
local playerSource = source
|
||
plate = MathTrim(plate)
|
||
|
||
local vehicleKey = GetVehicleFromPlate(plate)
|
||
if not vehicleKey then
|
||
return Error("Vehicle not spawned, plate:", plate)
|
||
end
|
||
|
||
local vehicleData = PersistentVehicles[vehicleKey]
|
||
local impound = GetImpoundOfType(vehicleData.type, vehicleData.vehicle.coords)
|
||
|
||
local updateQuery = string.format(
|
||
"UPDATE %s SET `garage` = ?, `%s` = ? WHERE plate = ?",
|
||
garageTable, storedColumn
|
||
)
|
||
MySQL.Sync.execute(updateQuery, { impound, State.impounded, vehicleData.vehicle.plate })
|
||
|
||
local entity = vehicleData.entity
|
||
if DoesEntityExist(entity) then DeleteEntity(entity) end
|
||
PersistentVehicles[vehicleKey] = nil
|
||
|
||
Notification(playerSource, i18n.t("vehicle_impounded", { garage = impound }), "success")
|
||
end)
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- SaveAllPersistentVehicles (batch SQL update)
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function SaveAllPersistentVehicles()
|
||
if type(PersistentVehicles) ~= "table" or next(PersistentVehicles) == nil then
|
||
return
|
||
end
|
||
|
||
local rowTemplate = "SELECT %s AS id, '%s' AS new_data"
|
||
local queryTemplate = [[
|
||
UPDATE %s u
|
||
JOIN (
|
||
%s
|
||
) a ON u.id = a.id
|
||
SET `%s` = new_data
|
||
]]
|
||
|
||
local unionParts = {}
|
||
for _, vehicleData in pairs(PersistentVehicles) do
|
||
local part = string.format(rowTemplate, vehicleData.id, json.encode(vehicleData.vehicle))
|
||
table.insert(unionParts, part)
|
||
end
|
||
|
||
local unionSQL = table.concat(unionParts, "\n\nUNION ")
|
||
local fullQuery = string.format(queryTemplate, garageTable, unionSQL, garagePropsColumn)
|
||
MySQL.Sync.execute(fullQuery)
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- Persistent threads (save + tick)
|
||
-- ──────────────────────────────────────────────
|
||
|
||
if Config.PersistentVehicles then
|
||
-- Save loop every 12s
|
||
CreateThread(function()
|
||
while true do
|
||
SaveAllPersistentVehicles()
|
||
Wait(12000)
|
||
end
|
||
end)
|
||
|
||
-- Property / respawn tick every 2s
|
||
CreateThread(function()
|
||
while true do
|
||
Wait(2000)
|
||
TickPersistentVehicles()
|
||
end
|
||
end)
|
||
end
|